mirror of
https://github.com/portainer/portainer.git
synced 2025-07-22 14:59:41 +02:00
feat(gpu) EE-3191 Add GPU support for containers (#7146)
This commit is contained in:
parent
f0456cbf5f
commit
4997e9c7be
43 changed files with 758 additions and 10 deletions
|
@ -103,6 +103,9 @@ func (m *Migrator) Migrate() error {
|
||||||
|
|
||||||
// Portainer 2.14
|
// Portainer 2.14
|
||||||
newMigration(50, m.migrateDBVersionToDB50),
|
newMigration(50, m.migrateDBVersionToDB50),
|
||||||
|
|
||||||
|
// Portainer 2.15
|
||||||
|
newMigration(60, m.migrateDBVersionToDB60),
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastDbVersion int
|
var lastDbVersion int
|
||||||
|
|
30
api/datastore/migrator/migrate_dbversion60.go
Normal file
30
api/datastore/migrator/migrate_dbversion60.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package migrator
|
||||||
|
|
||||||
|
import portainer "github.com/portainer/portainer/api"
|
||||||
|
|
||||||
|
func (m *Migrator) migrateDBVersionToDB60() error {
|
||||||
|
if err := m.addGpuInputFieldDB60(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Migrator) addGpuInputFieldDB60() error {
|
||||||
|
migrateLog.Info("- add gpu input field")
|
||||||
|
endpoints, err := m.endpointService.Endpoints()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
endpoint.Gpus = []portainer.Pair{}
|
||||||
|
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -43,6 +43,7 @@
|
||||||
},
|
},
|
||||||
"EdgeCheckinInterval": 0,
|
"EdgeCheckinInterval": 0,
|
||||||
"EdgeKey": "",
|
"EdgeKey": "",
|
||||||
|
"Gpus": [],
|
||||||
"GroupId": 1,
|
"GroupId": 1,
|
||||||
"Id": 1,
|
"Id": 1,
|
||||||
"IsEdgeDevice": false,
|
"IsEdgeDevice": false,
|
||||||
|
@ -175,6 +176,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DockerVersion": "20.10.13",
|
"DockerVersion": "20.10.13",
|
||||||
|
"GpuUseAll": false,
|
||||||
|
"GpuUseList": null,
|
||||||
"HealthyContainerCount": 0,
|
"HealthyContainerCount": 0,
|
||||||
"ImageCount": 9,
|
"ImageCount": 9,
|
||||||
"NodeCount": 0,
|
"NodeCount": 0,
|
||||||
|
|
|
@ -7,9 +7,10 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
_container "github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Snapshotter represents a service used to create environment(endpoint) snapshots
|
// Snapshotter represents a service used to create environment(endpoint) snapshots
|
||||||
|
@ -154,11 +155,35 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
|
||||||
healthyContainers := 0
|
healthyContainers := 0
|
||||||
unhealthyContainers := 0
|
unhealthyContainers := 0
|
||||||
stacks := make(map[string]struct{})
|
stacks := make(map[string]struct{})
|
||||||
|
gpuUseSet := make(map[string]struct{})
|
||||||
|
gpuUseAll := false
|
||||||
for _, container := range containers {
|
for _, container := range containers {
|
||||||
if container.State == "exited" {
|
if container.State == "exited" {
|
||||||
stoppedContainers++
|
stoppedContainers++
|
||||||
} else if container.State == "running" {
|
} else if container.State == "running" {
|
||||||
runningContainers++
|
runningContainers++
|
||||||
|
|
||||||
|
// snapshot GPUs
|
||||||
|
response, err := cli.ContainerInspect(context.Background(), container.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var gpuOptions *_container.DeviceRequest = nil
|
||||||
|
for _, deviceRequest := range response.HostConfig.Resources.DeviceRequests {
|
||||||
|
if deviceRequest.Driver == "nvidia" || deviceRequest.Capabilities[0][0] == "gpu" {
|
||||||
|
gpuOptions = &deviceRequest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if gpuOptions != nil {
|
||||||
|
if gpuOptions.Count == -1 {
|
||||||
|
gpuUseAll = true
|
||||||
|
}
|
||||||
|
for _, id := range gpuOptions.DeviceIDs {
|
||||||
|
gpuUseSet[id] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(container.Status, "(healthy)") {
|
if strings.Contains(container.Status, "(healthy)") {
|
||||||
|
@ -174,6 +199,14 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gpuUseList := make([]string, 0, len(gpuUseSet))
|
||||||
|
for gpuUse := range gpuUseSet {
|
||||||
|
gpuUseList = append(gpuUseList, gpuUse)
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.GpuUseAll = gpuUseAll
|
||||||
|
snapshot.GpuUseList = gpuUseList
|
||||||
|
|
||||||
snapshot.RunningContainerCount = runningContainers
|
snapshot.RunningContainerCount = runningContainers
|
||||||
snapshot.StoppedContainerCount = stoppedContainers
|
snapshot.StoppedContainerCount = stoppedContainers
|
||||||
snapshot.HealthyContainerCount = healthyContainers
|
snapshot.HealthyContainerCount = healthyContainers
|
||||||
|
|
|
@ -25,6 +25,7 @@ type endpointCreatePayload struct {
|
||||||
URL string
|
URL string
|
||||||
EndpointCreationType endpointCreationEnum
|
EndpointCreationType endpointCreationEnum
|
||||||
PublicURL string
|
PublicURL string
|
||||||
|
Gpus []portainer.Pair
|
||||||
GroupID int
|
GroupID int
|
||||||
TLS bool
|
TLS bool
|
||||||
TLSSkipVerify bool
|
TLSSkipVerify bool
|
||||||
|
@ -142,6 +143,13 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||||
payload.PublicURL = publicURL
|
payload.PublicURL = publicURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gpus := make([]portainer.Pair, 0)
|
||||||
|
err = request.RetrieveMultiPartFormJSONValue(r, "Gpus", &gpus, true)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("Invalid Gpus parameter")
|
||||||
|
}
|
||||||
|
payload.Gpus = gpus
|
||||||
|
|
||||||
checkinInterval, _ := request.RetrieveNumericMultiPartFormValue(r, "CheckinInterval", true)
|
checkinInterval, _ := request.RetrieveNumericMultiPartFormValue(r, "CheckinInterval", true)
|
||||||
payload.EdgeCheckinInterval = checkinInterval
|
payload.EdgeCheckinInterval = checkinInterval
|
||||||
|
|
||||||
|
@ -290,6 +298,7 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
|
||||||
Type: portainer.AzureEnvironment,
|
Type: portainer.AzureEnvironment,
|
||||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||||
PublicURL: payload.PublicURL,
|
PublicURL: payload.PublicURL,
|
||||||
|
Gpus: payload.Gpus,
|
||||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||||
AzureCredentials: credentials,
|
AzureCredentials: credentials,
|
||||||
|
@ -323,6 +332,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
||||||
URL: portainerHost,
|
URL: portainerHost,
|
||||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||||
|
Gpus: payload.Gpus,
|
||||||
TLSConfig: portainer.TLSConfiguration{
|
TLSConfig: portainer.TLSConfiguration{
|
||||||
TLS: false,
|
TLS: false,
|
||||||
},
|
},
|
||||||
|
@ -378,6 +388,7 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload)
|
||||||
Type: endpointType,
|
Type: endpointType,
|
||||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||||
PublicURL: payload.PublicURL,
|
PublicURL: payload.PublicURL,
|
||||||
|
Gpus: payload.Gpus,
|
||||||
TLSConfig: portainer.TLSConfiguration{
|
TLSConfig: portainer.TLSConfiguration{
|
||||||
TLS: false,
|
TLS: false,
|
||||||
},
|
},
|
||||||
|
@ -412,6 +423,7 @@ func (handler *Handler) createKubernetesEndpoint(payload *endpointCreatePayload)
|
||||||
Type: portainer.KubernetesLocalEnvironment,
|
Type: portainer.KubernetesLocalEnvironment,
|
||||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||||
PublicURL: payload.PublicURL,
|
PublicURL: payload.PublicURL,
|
||||||
|
Gpus: payload.Gpus,
|
||||||
TLSConfig: portainer.TLSConfiguration{
|
TLSConfig: portainer.TLSConfiguration{
|
||||||
TLS: payload.TLS,
|
TLS: payload.TLS,
|
||||||
TLSSkipVerify: payload.TLSSkipVerify,
|
TLSSkipVerify: payload.TLSSkipVerify,
|
||||||
|
@ -441,6 +453,7 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload,
|
||||||
Type: endpointType,
|
Type: endpointType,
|
||||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||||
PublicURL: payload.PublicURL,
|
PublicURL: payload.PublicURL,
|
||||||
|
Gpus: payload.Gpus,
|
||||||
TLSConfig: portainer.TLSConfiguration{
|
TLSConfig: portainer.TLSConfiguration{
|
||||||
TLS: payload.TLS,
|
TLS: payload.TLS,
|
||||||
TLSSkipVerify: payload.TLSSkipVerify,
|
TLSSkipVerify: payload.TLSSkipVerify,
|
||||||
|
|
|
@ -22,6 +22,8 @@ type endpointUpdatePayload struct {
|
||||||
// URL or IP address where exposed containers will be reachable.\
|
// URL or IP address where exposed containers will be reachable.\
|
||||||
// Defaults to URL if not specified
|
// Defaults to URL if not specified
|
||||||
PublicURL *string `example:"docker.mydomain.tld:2375"`
|
PublicURL *string `example:"docker.mydomain.tld:2375"`
|
||||||
|
// GPUs information
|
||||||
|
Gpus []portainer.Pair
|
||||||
// Group identifier
|
// Group identifier
|
||||||
GroupID *int `example:"1"`
|
GroupID *int `example:"1"`
|
||||||
// Require TLS to connect against this environment(endpoint)
|
// Require TLS to connect against this environment(endpoint)
|
||||||
|
@ -110,6 +112,10 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
endpoint.PublicURL = *payload.PublicURL
|
endpoint.PublicURL = *payload.PublicURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.Gpus != nil {
|
||||||
|
endpoint.Gpus = payload.Gpus
|
||||||
|
}
|
||||||
|
|
||||||
if payload.EdgeCheckinInterval != nil {
|
if payload.EdgeCheckinInterval != nil {
|
||||||
endpoint.EdgeCheckinInterval = *payload.EdgeCheckinInterval
|
endpoint.EdgeCheckinInterval = *payload.EdgeCheckinInterval
|
||||||
}
|
}
|
||||||
|
|
|
@ -199,6 +199,8 @@ type (
|
||||||
StackCount int `json:"StackCount"`
|
StackCount int `json:"StackCount"`
|
||||||
SnapshotRaw DockerSnapshotRaw `json:"DockerSnapshotRaw"`
|
SnapshotRaw DockerSnapshotRaw `json:"DockerSnapshotRaw"`
|
||||||
NodeCount int `json:"NodeCount"`
|
NodeCount int `json:"NodeCount"`
|
||||||
|
GpuUseAll bool `json:"GpuUseAll"`
|
||||||
|
GpuUseList []string `json:"GpuUseList"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DockerSnapshotRaw represents all the information related to a snapshot as returned by the Docker API
|
// DockerSnapshotRaw represents all the information related to a snapshot as returned by the Docker API
|
||||||
|
@ -310,6 +312,7 @@ type (
|
||||||
GroupID EndpointGroupID `json:"GroupId" example:"1"`
|
GroupID EndpointGroupID `json:"GroupId" example:"1"`
|
||||||
// URL or IP address where exposed containers will be reachable
|
// URL or IP address where exposed containers will be reachable
|
||||||
PublicURL string `json:"PublicURL" example:"docker.mydomain.tld:2375"`
|
PublicURL string `json:"PublicURL" example:"docker.mydomain.tld:2375"`
|
||||||
|
Gpus []Pair `json:"Gpus"`
|
||||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||||
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty" example:""`
|
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty" example:""`
|
||||||
// List of tag identifiers to which this environment(endpoint) is associated
|
// List of tag identifiers to which this environment(endpoint) is associated
|
||||||
|
|
|
@ -693,6 +693,12 @@ definitions:
|
||||||
$ref: '#/definitions/portainer.DockerSnapshotRaw'
|
$ref: '#/definitions/portainer.DockerSnapshotRaw'
|
||||||
DockerVersion:
|
DockerVersion:
|
||||||
type: string
|
type: string
|
||||||
|
GpuUseAll:
|
||||||
|
type: boolean
|
||||||
|
GpuUseList:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
HealthyContainerCount:
|
HealthyContainerCount:
|
||||||
type: integer
|
type: integer
|
||||||
ImageCount:
|
ImageCount:
|
||||||
|
@ -849,6 +855,11 @@ definitions:
|
||||||
EdgeKey:
|
EdgeKey:
|
||||||
description: The key which is used to map the agent to Portainer
|
description: The key which is used to map the agent to Portainer
|
||||||
type: string
|
type: string
|
||||||
|
Gpus:
|
||||||
|
description: Endpoint Gpus information
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/portainer.Pair'
|
||||||
|
type: array
|
||||||
GroupId:
|
GroupId:
|
||||||
description: Endpoint group identifier
|
description: Endpoint group identifier
|
||||||
example: 1
|
example: 1
|
||||||
|
|
210
app/docker/react/views/gpu.tsx
Normal file
210
app/docker/react/views/gpu.tsx
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { components } from 'react-select';
|
||||||
|
import { OnChangeValue } from 'react-select/dist/declarations/src/types';
|
||||||
|
import { OptionProps } from 'react-select/dist/declarations/src/components/Option';
|
||||||
|
|
||||||
|
import { Select } from '@@/form-components/ReactSelect';
|
||||||
|
import { Switch } from '@@/form-components/SwitchField/Switch';
|
||||||
|
import { Tooltip } from '@@/Tip/Tooltip';
|
||||||
|
|
||||||
|
interface Values {
|
||||||
|
enabled: boolean;
|
||||||
|
selectedGPUs: string[];
|
||||||
|
capabilities: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GpuOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GPU {
|
||||||
|
value: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
values: Values;
|
||||||
|
onChange(values: Values): void;
|
||||||
|
gpus: GPU[];
|
||||||
|
usedGpus: string[];
|
||||||
|
usedAllGpus: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NvidiaCapabilitiesOptions = [
|
||||||
|
// Taken from https://github.com/containerd/containerd/blob/master/contrib/nvidia/nvidia.go#L40
|
||||||
|
{
|
||||||
|
value: 'compute',
|
||||||
|
label: 'compute',
|
||||||
|
description: 'required for CUDA and OpenCL applications',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'compat32',
|
||||||
|
label: 'compat32',
|
||||||
|
description: 'required for running 32-bit applications',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'graphics',
|
||||||
|
label: 'graphics',
|
||||||
|
description: 'required for running OpenGL and Vulkan applications',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'utility',
|
||||||
|
label: 'utility',
|
||||||
|
description: 'required for using nvidia-smi and NVML',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'video',
|
||||||
|
label: 'video',
|
||||||
|
description: 'required for using the Video Codec SDK',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'display',
|
||||||
|
label: 'display',
|
||||||
|
description: 'required for leveraging X11 display',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function Option(props: OptionProps<GpuOption, true>) {
|
||||||
|
const {
|
||||||
|
data: { value, description },
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||||
|
<components.Option {...props}>
|
||||||
|
{`${value} - ${description}`}
|
||||||
|
</components.Option>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Gpu({
|
||||||
|
values,
|
||||||
|
onChange,
|
||||||
|
gpus,
|
||||||
|
usedGpus = [],
|
||||||
|
usedAllGpus,
|
||||||
|
}: Props) {
|
||||||
|
const options = useMemo(() => {
|
||||||
|
const options = gpus.map((gpu) => ({
|
||||||
|
value: gpu.value,
|
||||||
|
label:
|
||||||
|
usedGpus.includes(gpu.value) || usedAllGpus
|
||||||
|
? `${gpu.name} (in use)`
|
||||||
|
: gpu.name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}, [gpus, usedGpus, usedAllGpus]);
|
||||||
|
|
||||||
|
function onChangeValues(key: string, newValue: boolean | string[]) {
|
||||||
|
const newValues = {
|
||||||
|
...values,
|
||||||
|
[key]: newValue,
|
||||||
|
};
|
||||||
|
onChange(newValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleEnableGpu() {
|
||||||
|
onChangeValues('enabled', !values.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChangeSelectedGpus(newValue: OnChangeValue<GpuOption, true>) {
|
||||||
|
onChangeValues(
|
||||||
|
'selectedGPUs',
|
||||||
|
newValue.map((option) => option.value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChangeSelectedCaps(newValue: OnChangeValue<GpuOption, true>) {
|
||||||
|
onChangeValues(
|
||||||
|
'capabilities',
|
||||||
|
newValue.map((option) => option.value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const gpuCmd = useMemo(() => {
|
||||||
|
const devices = values.selectedGPUs.join(',');
|
||||||
|
const caps = values.capabilities.join(',');
|
||||||
|
return `--gpus 'device=${devices},"capabilities=${caps}"`;
|
||||||
|
}, [values.selectedGPUs, values.capabilities]);
|
||||||
|
|
||||||
|
const gpuValue = useMemo(
|
||||||
|
() =>
|
||||||
|
options.filter((option) => values.selectedGPUs.includes(option.value)),
|
||||||
|
[values.selectedGPUs, options]
|
||||||
|
);
|
||||||
|
|
||||||
|
const capValue = useMemo(
|
||||||
|
() =>
|
||||||
|
NvidiaCapabilitiesOptions.filter((option) =>
|
||||||
|
values.capabilities.includes(option.value)
|
||||||
|
),
|
||||||
|
[values.capabilities]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="form-group">
|
||||||
|
<div className="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Enable GPU
|
||||||
|
<Switch
|
||||||
|
id="enabled"
|
||||||
|
name="enabled"
|
||||||
|
checked={values.enabled}
|
||||||
|
onChange={toggleEnableGpu}
|
||||||
|
className="ml-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-sm-9 col-lg-10 text-left">
|
||||||
|
<Select<GpuOption, true>
|
||||||
|
isMulti
|
||||||
|
closeMenuOnSelect
|
||||||
|
value={gpuValue}
|
||||||
|
isDisabled={!values.enabled}
|
||||||
|
onChange={onChangeSelectedGpus}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{values.enabled && (
|
||||||
|
<>
|
||||||
|
<div className="form-group">
|
||||||
|
<div className="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Capabilities
|
||||||
|
<Tooltip
|
||||||
|
message={
|
||||||
|
"This is the generated equivalent of the '--gpus' docker CLI parameter based on your settings."
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-sm-9 col-lg-10 text-left">
|
||||||
|
<Select<GpuOption, true>
|
||||||
|
isMulti
|
||||||
|
closeMenuOnSelect
|
||||||
|
value={capValue}
|
||||||
|
options={NvidiaCapabilitiesOptions}
|
||||||
|
components={{ Option }}
|
||||||
|
onChange={onChangeSelectedCaps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<div className="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Control
|
||||||
|
<Tooltip message="This is the generated equivalent of the '--gpus' docker CLI parameter based on your settings." />
|
||||||
|
</div>
|
||||||
|
<div className="col-sm-9 col-lg-10">
|
||||||
|
<code>{gpuCmd}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,8 +1,13 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
import { Gpu } from 'Docker/react/views/gpu';
|
||||||
|
|
||||||
import { ItemView } from '@/react/docker/networks/ItemView';
|
import { ItemView } from '@/react/docker/networks/ItemView';
|
||||||
import { r2a } from '@/react-tools/react2angular';
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
|
|
||||||
export const viewsModule = angular
|
export const viewsModule = angular
|
||||||
.module('portainer.docker.react.views', [])
|
.module('portainer.docker.react.views', [])
|
||||||
|
.component(
|
||||||
|
'gpu',
|
||||||
|
r2a(Gpu, ['values', 'onChange', 'gpus', 'usedGpus', 'usedAllGpus'])
|
||||||
|
)
|
||||||
.component('networkDetailsView', r2a(ItemView, [])).name;
|
.component('networkDetailsView', r2a(ItemView, [])).name;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
angular.module('portainer.docker').controller('ContainersController', ContainersController);
|
angular.module('portainer.docker').controller('ContainersController', ContainersController);
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
function ContainersController($scope, ContainerService, Notifications, endpoint) {
|
function ContainersController($scope, ContainerService, Notifications, endpoint) {
|
||||||
|
@ -8,9 +9,42 @@ function ContainersController($scope, ContainerService, Notifications, endpoint)
|
||||||
$scope.getContainers = getContainers;
|
$scope.getContainers = getContainers;
|
||||||
|
|
||||||
function getContainers() {
|
function getContainers() {
|
||||||
|
$scope.containers = null;
|
||||||
|
$scope.containers_t = null;
|
||||||
ContainerService.containers(1)
|
ContainerService.containers(1)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.containers = data;
|
$scope.containers_t = data;
|
||||||
|
if ($scope.containers_t.length === 0) {
|
||||||
|
$scope.containers = $scope.containers_t;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let item of $scope.containers_t) {
|
||||||
|
ContainerService.container(item.Id).then(function success(data) {
|
||||||
|
var Id = data.Id;
|
||||||
|
for (var i = 0; i < $scope.containers_t.length; i++) {
|
||||||
|
if (Id == $scope.containers_t[i].Id) {
|
||||||
|
const gpuOptions = _.find(data.HostConfig.DeviceRequests, function (o) {
|
||||||
|
return o.Driver === 'nvidia' || o.Capabilities[0][0] === 'gpu';
|
||||||
|
});
|
||||||
|
if (!gpuOptions) {
|
||||||
|
$scope.containers_t[i]['Gpus'] = 'none';
|
||||||
|
} else {
|
||||||
|
let gpuStr = 'all';
|
||||||
|
if (gpuOptions.Count !== -1) {
|
||||||
|
gpuStr = `id:${_.join(gpuOptions.DeviceIDs, ',')}`;
|
||||||
|
}
|
||||||
|
$scope.containers_t[i]['Gpus'] = `${gpuStr}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let item of $scope.containers_t) {
|
||||||
|
if (!Object.keys(item).includes('Gpus')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$scope.containers = $scope.containers_t;
|
||||||
|
});
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve containers');
|
Notifications.error('Failure', err, 'Unable to retrieve containers');
|
||||||
|
|
|
@ -69,6 +69,12 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
$scope.containerWebhookFeature = FeatureId.CONTAINER_WEBHOOK;
|
$scope.containerWebhookFeature = FeatureId.CONTAINER_WEBHOOK;
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
alwaysPull: true,
|
alwaysPull: true,
|
||||||
|
GPU: {
|
||||||
|
enabled: false,
|
||||||
|
useSpecific: false,
|
||||||
|
selectedGPUs: [],
|
||||||
|
capabilities: ['compute', 'utility'],
|
||||||
|
},
|
||||||
Console: 'none',
|
Console: 'none',
|
||||||
Volumes: [],
|
Volumes: [],
|
||||||
NetworkContainer: null,
|
NetworkContainer: null,
|
||||||
|
@ -149,6 +155,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
Runtime: null,
|
Runtime: null,
|
||||||
ExtraHosts: [],
|
ExtraHosts: [],
|
||||||
Devices: [],
|
Devices: [],
|
||||||
|
DeviceRequests: [],
|
||||||
CapAdd: [],
|
CapAdd: [],
|
||||||
CapDrop: [],
|
CapDrop: [],
|
||||||
Sysctls: {},
|
Sysctls: {},
|
||||||
|
@ -199,6 +206,12 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
$scope.config.HostConfig.Devices.splice(index, 1);
|
$scope.config.HostConfig.Devices.splice(index, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.onGpuChange = function (values) {
|
||||||
|
return $async(async () => {
|
||||||
|
$scope.formValues.GPU = values;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
$scope.addSysctl = function () {
|
$scope.addSysctl = function () {
|
||||||
$scope.formValues.Sysctls.push({ name: '', value: '' });
|
$scope.formValues.Sysctls.push({ name: '', value: '' });
|
||||||
};
|
};
|
||||||
|
@ -417,6 +430,36 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
config.HostConfig.CapDrop = notAllowed.map(getCapName);
|
config.HostConfig.CapDrop = notAllowed.map(getCapName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function prepareGPUOptions(config) {
|
||||||
|
const driver = 'nvidia';
|
||||||
|
const gpuOptions = $scope.formValues.GPU;
|
||||||
|
|
||||||
|
const existingDeviceRequest = _.find($scope.config.HostConfig.DeviceRequests, function (o) {
|
||||||
|
return o.Driver === driver || o.Capabilities[0][0] === 'gpu';
|
||||||
|
});
|
||||||
|
if (existingDeviceRequest) {
|
||||||
|
_.pullAllBy(config.HostConfig.DeviceRequests, [existingDeviceRequest], 'Driver');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!gpuOptions.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceRequest = existingDeviceRequest || {
|
||||||
|
Driver: driver,
|
||||||
|
Count: -1,
|
||||||
|
DeviceIDs: [], // must be empty if Count != 0 https://github.com/moby/moby/blob/master/daemon/nvidia_linux.go#L50
|
||||||
|
Capabilities: [], // array of ORed arrays of ANDed capabilites = [ [c1 AND c2] OR [c1 AND c3] ] : https://github.com/moby/moby/blob/master/api/types/container/host_config.go#L272
|
||||||
|
// Options: { property1: "string", property2: "string" }, // seems to never be evaluated/used in docker API ?
|
||||||
|
};
|
||||||
|
|
||||||
|
deviceRequest.DeviceIDs = gpuOptions.selectedGPUs;
|
||||||
|
deviceRequest.Count = 0;
|
||||||
|
deviceRequest.Capabilities = [gpuOptions.capabilities];
|
||||||
|
|
||||||
|
config.HostConfig.DeviceRequests.push(deviceRequest);
|
||||||
|
}
|
||||||
|
|
||||||
function prepareConfiguration() {
|
function prepareConfiguration() {
|
||||||
var config = angular.copy($scope.config);
|
var config = angular.copy($scope.config);
|
||||||
prepareCmd(config);
|
prepareCmd(config);
|
||||||
|
@ -433,6 +476,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
prepareLogDriver(config);
|
prepareLogDriver(config);
|
||||||
prepareCapabilities(config);
|
prepareCapabilities(config);
|
||||||
prepareSysctls(config);
|
prepareSysctls(config);
|
||||||
|
prepareGPUOptions(config);
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -571,6 +615,24 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
$scope.config.HostConfig.Devices = path;
|
$scope.config.HostConfig.Devices = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadFromContainerDeviceRequests() {
|
||||||
|
const deviceRequest = _.find($scope.config.HostConfig.DeviceRequests, function (o) {
|
||||||
|
return o.Driver === 'nvidia' || o.Capabilities[0][0] === 'gpu';
|
||||||
|
});
|
||||||
|
if (deviceRequest) {
|
||||||
|
$scope.formValues.GPU.enabled = true;
|
||||||
|
$scope.formValues.GPU.useSpecific = deviceRequest.Count !== -1;
|
||||||
|
$scope.formValues.GPU.selectedGPUs = deviceRequest.DeviceIDs || [];
|
||||||
|
if ($scope.formValues.GPU.useSpecific) {
|
||||||
|
$scope.formValues.GPU.selectedGPUs = deviceRequest.DeviceIDs;
|
||||||
|
}
|
||||||
|
// we only support a single set of capabilities for now
|
||||||
|
// UI needs to be reworked in order to support OR combinations of AND capabilities
|
||||||
|
$scope.formValues.GPU.capabilities = deviceRequest.Capabilities[0];
|
||||||
|
$scope.formValues.GPU = { ...$scope.formValues.GPU };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function loadFromContainerSysctls() {
|
function loadFromContainerSysctls() {
|
||||||
for (var s in $scope.config.HostConfig.Sysctls) {
|
for (var s in $scope.config.HostConfig.Sysctls) {
|
||||||
if ({}.hasOwnProperty.call($scope.config.HostConfig.Sysctls, s)) {
|
if ({}.hasOwnProperty.call($scope.config.HostConfig.Sysctls, s)) {
|
||||||
|
@ -651,6 +713,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
loadFromContainerLabels(d);
|
loadFromContainerLabels(d);
|
||||||
loadFromContainerConsole(d);
|
loadFromContainerConsole(d);
|
||||||
loadFromContainerDevices(d);
|
loadFromContainerDevices(d);
|
||||||
|
loadFromContainerDeviceRequests(d);
|
||||||
loadFromContainerImageConfig(d);
|
loadFromContainerImageConfig(d);
|
||||||
loadFromContainerResources(d);
|
loadFromContainerResources(d);
|
||||||
loadFromContainerCapabilities(d);
|
loadFromContainerCapabilities(d);
|
||||||
|
@ -715,6 +778,8 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
function (d) {
|
function (d) {
|
||||||
var containers = d;
|
var containers = d;
|
||||||
$scope.runningContainers = containers;
|
$scope.runningContainers = containers;
|
||||||
|
$scope.gpuUseAll = $scope.endpoint.Snapshots[0].GpuUseAll;
|
||||||
|
$scope.gpuUseList = $scope.endpoint.Snapshots[0].GpuUseList;
|
||||||
if ($transition$.params().from) {
|
if ($transition$.params().from) {
|
||||||
loadFromContainerSpec();
|
loadFromContainerSpec();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -693,6 +693,20 @@
|
||||||
<!-- !sysctls-input-list -->
|
<!-- !sysctls-input-list -->
|
||||||
</div>
|
</div>
|
||||||
<!-- !sysctls -->
|
<!-- !sysctls -->
|
||||||
|
<!-- #region GPU -->
|
||||||
|
<div class="col-sm-12 form-section-title"> GPU </div>
|
||||||
|
|
||||||
|
<gpu
|
||||||
|
ng-if="applicationState.endpoint.apiVersion >= 1.4"
|
||||||
|
values="formValues.GPU"
|
||||||
|
on-change="(onGpuChange)"
|
||||||
|
gpus="endpoint.Gpus"
|
||||||
|
used-gpus="gpuUseList"
|
||||||
|
used-all-gpus="gpuUseAll"
|
||||||
|
>
|
||||||
|
</gpu>
|
||||||
|
|
||||||
|
<!-- #endregion GPU -->
|
||||||
<div ng-class="{ 'edit-resources': state.mode == 'duplicate' }">
|
<div ng-class="{ 'edit-resources': state.mode == 'duplicate' }">
|
||||||
<div class="col-sm-12 form-section-title"> Resources </div>
|
<div class="col-sm-12 form-section-title"> Resources </div>
|
||||||
<!-- memory-reservation-input -->
|
<!-- memory-reservation-input -->
|
||||||
|
|
|
@ -322,6 +322,10 @@
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr ng-if="container.HostConfig.DeviceRequests.length">
|
||||||
|
<td>GPUS</td>
|
||||||
|
<td>{{ computeDockerGPUCommand() }}</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
|
|
|
@ -77,6 +77,23 @@ angular.module('portainer.docker').controller('ContainerController', [
|
||||||
$state.reload();
|
$state.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.computeDockerGPUCommand = () => {
|
||||||
|
const gpuOptions = _.find($scope.container.HostConfig.DeviceRequests, function (o) {
|
||||||
|
return o.Driver === 'nvidia' || o.Capabilities[0][0] === 'gpu';
|
||||||
|
});
|
||||||
|
if (!gpuOptions) {
|
||||||
|
return 'No GPU config found';
|
||||||
|
}
|
||||||
|
let gpuStr = 'all';
|
||||||
|
if (gpuOptions.Count !== -1) {
|
||||||
|
gpuStr = `"device=${_.join(gpuOptions.DeviceIDs, ',')}"`;
|
||||||
|
}
|
||||||
|
// we only support a single set of capabilities for now
|
||||||
|
// creation UI needs to be reworked in order to support OR combinations of AND capabilities
|
||||||
|
const capStr = `"capabilities=${_.join(gpuOptions.Capabilities[0], ',')}"`;
|
||||||
|
return `${gpuStr},${capStr}`;
|
||||||
|
};
|
||||||
|
|
||||||
var update = function () {
|
var update = function () {
|
||||||
var nodeName = $transition$.params().nodeName;
|
var nodeName = $transition$.params().nodeName;
|
||||||
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
|
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
|
||||||
|
|
|
@ -53,6 +53,10 @@
|
||||||
<td>URL</td>
|
<td>URL</td>
|
||||||
<td>{{ endpoint.URL | stripprotocol }}</td>
|
<td>{{ endpoint.URL | stripprotocol }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ endpoint.Gpus.length <= 1 ? 'GPU' : 'GPUs' }}</td>
|
||||||
|
<td>{{ gpuInfoStr }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Tags</td>
|
<td>Tags</td>
|
||||||
<td>{{ endpointTags }}</td>
|
<td>{{ endpointTags }}</td>
|
||||||
|
@ -97,5 +101,9 @@
|
||||||
<a ui-sref="docker.networks">
|
<a ui-sref="docker.networks">
|
||||||
<dashboard-item icon="'fa-sitemap'" type="'Network'" value="networkCount"></dashboard-item>
|
<dashboard-item icon="'fa-sitemap'" type="'Network'" value="networkCount"></dashboard-item>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<dashboard-item icon="'fa-digital-tachograph'" type="'GPU'" value="endpoint.Gpus.length"></dashboard-item>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -44,6 +44,36 @@ angular.module('portainer.docker').controller('DashboardController', [
|
||||||
$scope.offlineMode = false;
|
$scope.offlineMode = false;
|
||||||
$scope.showStacks = false;
|
$scope.showStacks = false;
|
||||||
|
|
||||||
|
$scope.buildGpusStr = function (gpuUseSet) {
|
||||||
|
var gpusAvailable = new Object();
|
||||||
|
for (let i = 0; i < $scope.endpoint.Gpus.length; i++) {
|
||||||
|
if (!gpuUseSet.has($scope.endpoint.Gpus[i].name)) {
|
||||||
|
var exist = false;
|
||||||
|
for (let gpuAvailable in gpusAvailable) {
|
||||||
|
if ($scope.endpoint.Gpus[i].value == gpuAvailable) {
|
||||||
|
gpusAvailable[gpuAvailable] += 1;
|
||||||
|
exist = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (exist === false) {
|
||||||
|
gpusAvailable[$scope.endpoint.Gpus[i].value] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var retStr = Object.keys(gpusAvailable).length
|
||||||
|
? _.join(
|
||||||
|
_.map(Object.keys(gpusAvailable), (gpuAvailable) => {
|
||||||
|
var _str = gpusAvailable[gpuAvailable];
|
||||||
|
_str += ' x ';
|
||||||
|
_str += gpuAvailable;
|
||||||
|
return _str;
|
||||||
|
}),
|
||||||
|
' + '
|
||||||
|
)
|
||||||
|
: 'none';
|
||||||
|
return retStr;
|
||||||
|
};
|
||||||
|
|
||||||
async function initView() {
|
async function initView() {
|
||||||
const endpointMode = $scope.applicationState.endpoint.mode;
|
const endpointMode = $scope.applicationState.endpoint.mode;
|
||||||
$scope.endpoint = endpoint;
|
$scope.endpoint = endpoint;
|
||||||
|
@ -72,6 +102,14 @@ angular.module('portainer.docker').controller('DashboardController', [
|
||||||
$scope.serviceCount = data.services.length;
|
$scope.serviceCount = data.services.length;
|
||||||
$scope.stackCount = data.stacks.length;
|
$scope.stackCount = data.stacks.length;
|
||||||
$scope.info = data.info;
|
$scope.info = data.info;
|
||||||
|
|
||||||
|
$scope.gpuInfoStr = $scope.buildGpusStr(new Set());
|
||||||
|
$scope.gpuUseAll = _.get($scope, 'endpoint.Snapshots[0].GpuUseAll', false);
|
||||||
|
$scope.gpuUseList = _.get($scope, 'endpoint.Snapshots[0].GpuUseList', []);
|
||||||
|
$scope.gpuFreeStr = 'all';
|
||||||
|
if ($scope.gpuUseAll == true) $scope.gpuFreeStr = 'none';
|
||||||
|
else $scope.gpuFreeStr = $scope.buildGpusStr(new Set($scope.gpuUseList));
|
||||||
|
|
||||||
$scope.endpointTags = endpoint.TagIds.length
|
$scope.endpointTags = endpoint.TagIds.length
|
||||||
? _.join(
|
? _.join(
|
||||||
_.filter(
|
_.filter(
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
||||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
import { type EnvironmentGroupId } from '@/portainer/environment-groups/types';
|
import { type EnvironmentGroupId } from '@/portainer/environment-groups/types';
|
||||||
import { type TagId } from '@/portainer/tags/types';
|
import { type TagId } from '@/portainer/tags/types';
|
||||||
|
@ -16,6 +17,7 @@ interface CreateLocalDockerEnvironment {
|
||||||
socketPath?: string;
|
socketPath?: string;
|
||||||
publicUrl?: string;
|
publicUrl?: string;
|
||||||
meta?: EnvironmentMetadata;
|
meta?: EnvironmentMetadata;
|
||||||
|
gpus?: Gpu[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createLocalDockerEnvironment({
|
export async function createLocalDockerEnvironment({
|
||||||
|
@ -23,6 +25,7 @@ export async function createLocalDockerEnvironment({
|
||||||
socketPath = '',
|
socketPath = '',
|
||||||
publicUrl = '',
|
publicUrl = '',
|
||||||
meta = { tagIds: [] },
|
meta = { tagIds: [] },
|
||||||
|
gpus = [],
|
||||||
}: CreateLocalDockerEnvironment) {
|
}: CreateLocalDockerEnvironment) {
|
||||||
const url = prefixPath(socketPath);
|
const url = prefixPath(socketPath);
|
||||||
|
|
||||||
|
@ -33,6 +36,7 @@ export async function createLocalDockerEnvironment({
|
||||||
url,
|
url,
|
||||||
publicUrl,
|
publicUrl,
|
||||||
meta,
|
meta,
|
||||||
|
gpus,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -105,6 +109,7 @@ export interface EnvironmentOptions {
|
||||||
azure?: AzureSettings;
|
azure?: AzureSettings;
|
||||||
tls?: TLSSettings;
|
tls?: TLSSettings;
|
||||||
isEdgeDevice?: boolean;
|
isEdgeDevice?: boolean;
|
||||||
|
gpus?: Gpu[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreateRemoteEnvironment {
|
interface CreateRemoteEnvironment {
|
||||||
|
@ -133,6 +138,7 @@ export interface CreateAgentEnvironmentValues {
|
||||||
name: string;
|
name: string;
|
||||||
environmentUrl: string;
|
environmentUrl: string;
|
||||||
meta: EnvironmentMetadata;
|
meta: EnvironmentMetadata;
|
||||||
|
gpus: Gpu[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAgentEnvironment({
|
export function createAgentEnvironment({
|
||||||
|
@ -159,12 +165,14 @@ interface CreateEdgeAgentEnvironment {
|
||||||
portainerUrl: string;
|
portainerUrl: string;
|
||||||
meta?: EnvironmentMetadata;
|
meta?: EnvironmentMetadata;
|
||||||
pollFrequency: number;
|
pollFrequency: number;
|
||||||
|
gpus?: Gpu[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createEdgeAgentEnvironment({
|
export function createEdgeAgentEnvironment({
|
||||||
name,
|
name,
|
||||||
portainerUrl,
|
portainerUrl,
|
||||||
meta = { tagIds: [] },
|
meta = { tagIds: [] },
|
||||||
|
gpus = [],
|
||||||
}: CreateEdgeAgentEnvironment) {
|
}: CreateEdgeAgentEnvironment) {
|
||||||
return createEnvironment(
|
return createEnvironment(
|
||||||
name,
|
name,
|
||||||
|
@ -176,6 +184,7 @@ export function createEdgeAgentEnvironment({
|
||||||
skipVerify: true,
|
skipVerify: true,
|
||||||
skipClientVerify: true,
|
skipClientVerify: true,
|
||||||
},
|
},
|
||||||
|
gpus,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -201,6 +210,7 @@ async function createEnvironment(
|
||||||
TagIds: arrayToJson(tagIds),
|
TagIds: arrayToJson(tagIds),
|
||||||
CheckinInterval: options.checkinInterval,
|
CheckinInterval: options.checkinInterval,
|
||||||
IsEdgeDevice: options.isEdgeDevice,
|
IsEdgeDevice: options.isEdgeDevice,
|
||||||
|
Gpus: arrayToJson(options.gpus),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { tls, azure } = options;
|
const { tls, azure } = options;
|
||||||
|
|
|
@ -40,6 +40,8 @@ export interface DockerSnapshot {
|
||||||
ServiceCount: number;
|
ServiceCount: number;
|
||||||
Swarm: boolean;
|
Swarm: boolean;
|
||||||
DockerVersion: string;
|
DockerVersion: string;
|
||||||
|
GpuUseAll: boolean;
|
||||||
|
GpuUseList: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KubernetesSnapshot {
|
export interface KubernetesSnapshot {
|
||||||
|
@ -103,6 +105,7 @@ export type Environment = {
|
||||||
AMTDeviceGUID?: string;
|
AMTDeviceGUID?: string;
|
||||||
Edge: EnvironmentEdge;
|
Edge: EnvironmentEdge;
|
||||||
SecuritySettings: EnvironmentSecuritySettings;
|
SecuritySettings: EnvironmentSecuritySettings;
|
||||||
|
Gpus: { name: string; value: string }[];
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* TS reference of endpoint_create.go#EndpointCreationType iota
|
* TS reference of endpoint_create.go#EndpointCreationType iota
|
||||||
|
|
|
@ -100,6 +100,8 @@ export function EnvironmentItem({ environment, onClick, groupName }: Props) {
|
||||||
{environment.Snapshots[0].TotalCPU}
|
{environment.Snapshots[0].TotalCPU}
|
||||||
<i className="fa fa-memory space-left space-right" />
|
<i className="fa fa-memory space-left space-right" />
|
||||||
{humanize(environment.Snapshots[0].TotalMemory)}
|
{humanize(environment.Snapshots[0].TotalMemory)}
|
||||||
|
<i className="fa fa-digital-tachograph space-left space-right" />
|
||||||
|
{environment.Gpus.length}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="space-left space-right">-</span>
|
<span className="space-left space-right">-</span>
|
||||||
|
|
|
@ -222,6 +222,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !open-amt info -->
|
<!-- !open-amt info -->
|
||||||
|
<!-- gpus info -->
|
||||||
|
<div class="col-sm-12 form-section-title">Hardware acceleration</div>
|
||||||
|
<gpus-list value="endpoint.Gpus || []" on-change="(onGpusChange)"></gpus-list>
|
||||||
|
<!-- gpus info -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -9,8 +9,9 @@ import { confirmAsync } from '@/portainer/services/modal.service/confirm';
|
||||||
import { isEdgeEnvironment } from '@/portainer/environments/utils';
|
import { isEdgeEnvironment } from '@/portainer/environments/utils';
|
||||||
|
|
||||||
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
|
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
|
||||||
|
import { GpusListAngular } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
||||||
|
|
||||||
angular.module('portainer.app').controller('EndpointController', EndpointController);
|
angular.module('portainer.app').component('gpusList', GpusListAngular).controller('EndpointController', EndpointController);
|
||||||
|
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
function EndpointController(
|
function EndpointController(
|
||||||
|
@ -34,6 +35,12 @@ function EndpointController(
|
||||||
const isBE = process.env.PORTAINER_EDITION === 'BE';
|
const isBE = process.env.PORTAINER_EDITION === 'BE';
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
|
selectAll: false,
|
||||||
|
// displayTextFilter: false,
|
||||||
|
get selectedItemCount() {
|
||||||
|
return $scope.state.selectedItems.length || 0;
|
||||||
|
},
|
||||||
|
selectedItems: [],
|
||||||
uploadInProgress: false,
|
uploadInProgress: false,
|
||||||
actionInProgress: false,
|
actionInProgress: false,
|
||||||
azureEndpoint: false,
|
azureEndpoint: false,
|
||||||
|
@ -51,6 +58,46 @@ function EndpointController(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.selectAll = function () {
|
||||||
|
$scope.state.firstClickedItem = null;
|
||||||
|
for (var i = 0; i < $scope.state.filteredDataSet.length; i++) {
|
||||||
|
var item = $scope.state.filteredDataSet[i];
|
||||||
|
if (item.Checked !== $scope.state.selectAll) {
|
||||||
|
// if ($scope.allowSelection(item) && item.Checked !== $scope.state.selectAll) {
|
||||||
|
item.Checked = $scope.state.selectAll;
|
||||||
|
$scope.selectItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function isBetween(value, a, b) {
|
||||||
|
return (value >= a && value <= b) || (value >= b && value <= a);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.selectItem = function (item, event) {
|
||||||
|
// Handle range select using shift
|
||||||
|
if (event && event.originalEvent.shiftKey && $scope.state.firstClickedItem) {
|
||||||
|
const firstItemIndex = $scope.state.filteredDataSet.indexOf($scope.state.firstClickedItem);
|
||||||
|
const lastItemIndex = $scope.state.filteredDataSet.indexOf(item);
|
||||||
|
const itemsInRange = _.filter($scope.state.filteredDataSet, (item, index) => {
|
||||||
|
return isBetween(index, firstItemIndex, lastItemIndex);
|
||||||
|
});
|
||||||
|
const value = $scope.state.firstClickedItem.Checked;
|
||||||
|
|
||||||
|
_.forEach(itemsInRange, (i) => {
|
||||||
|
i.Checked = value;
|
||||||
|
});
|
||||||
|
$scope.state.firstClickedItem = item;
|
||||||
|
} else if (event) {
|
||||||
|
item.Checked = !item.Checked;
|
||||||
|
$scope.state.firstClickedItem = item;
|
||||||
|
}
|
||||||
|
$scope.state.selectedItems = _.uniq(_.concat($scope.state.selectedItems, $scope.state.filteredDataSet)).filter((i) => i.Checked);
|
||||||
|
if (event && $scope.state.selectAll && $scope.state.selectedItems.length !== $scope.state.filteredDataSet.length) {
|
||||||
|
$scope.state.selectAll = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
SecurityFormData: new EndpointSecurityFormData(),
|
SecurityFormData: new EndpointSecurityFormData(),
|
||||||
};
|
};
|
||||||
|
@ -106,6 +153,36 @@ function EndpointController(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope.onGpusChange = onGpusChange;
|
||||||
|
|
||||||
|
Array.prototype.indexOf = function (val) {
|
||||||
|
for (var i = 0; i < this.length; i++) {
|
||||||
|
if (this[i] == val) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
Array.prototype.remove = function (val) {
|
||||||
|
var index = this.indexOf(val);
|
||||||
|
if (index > -1) {
|
||||||
|
this.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function onGpusChange(value) {
|
||||||
|
return $async(async () => {
|
||||||
|
$scope.endpoint.Gpus = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyGpus() {
|
||||||
|
var i = $scope.endpoint.Gpus.length;
|
||||||
|
while (i--) {
|
||||||
|
if ($scope.endpoint.Gpus[i].name === '' || $scope.endpoint.Gpus[i].name === null) {
|
||||||
|
$scope.endpoint.Gpus.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$scope.updateEndpoint = async function () {
|
$scope.updateEndpoint = async function () {
|
||||||
var endpoint = $scope.endpoint;
|
var endpoint = $scope.endpoint;
|
||||||
var securityData = $scope.formValues.SecurityFormData;
|
var securityData = $scope.formValues.SecurityFormData;
|
||||||
|
@ -135,9 +212,11 @@ function EndpointController(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verifyGpus();
|
||||||
var payload = {
|
var payload = {
|
||||||
Name: endpoint.Name,
|
Name: endpoint.Name,
|
||||||
PublicURL: endpoint.PublicURL,
|
PublicURL: endpoint.PublicURL,
|
||||||
|
Gpus: endpoint.Gpus,
|
||||||
GroupID: endpoint.GroupId,
|
GroupID: endpoint.GroupId,
|
||||||
TagIds: endpoint.TagIds,
|
TagIds: endpoint.TagIds,
|
||||||
TLS: TLS,
|
TLS: TLS,
|
||||||
|
|
|
@ -91,5 +91,6 @@ export function createMockEnvironment(): Environment {
|
||||||
allowVolumeBrowserForRegularUsers: false,
|
allowVolumeBrowserForRegularUsers: false,
|
||||||
enableHostManagementFeatures: false,
|
enableHostManagementFeatures: false,
|
||||||
},
|
},
|
||||||
|
Gpus: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,12 +15,19 @@ export function useInputGroupContext() {
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
size?: Size;
|
size?: Size;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InputGroup({ children, size }: PropsWithChildren<Props>) {
|
export function InputGroup({
|
||||||
|
children,
|
||||||
|
size,
|
||||||
|
className,
|
||||||
|
}: PropsWithChildren<Props>) {
|
||||||
return (
|
return (
|
||||||
<Context.Provider value>
|
<Context.Provider value>
|
||||||
<div className={clsx('input-group', sizeClass(size))}>{children}</div>
|
<div className={clsx('input-group', sizeClass(size), className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</Context.Provider>
|
</Context.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ interface InputGroupSubComponents {
|
||||||
Addon: typeof InputGroupAddon;
|
Addon: typeof InputGroupAddon;
|
||||||
ButtonWrapper: typeof InputGroupButtonWrapper;
|
ButtonWrapper: typeof InputGroupButtonWrapper;
|
||||||
Input: typeof Input;
|
Input: typeof Input;
|
||||||
|
className: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InputGroup: typeof MainComponent & InputGroupSubComponents =
|
const InputGroup: typeof MainComponent & InputGroupSubComponents =
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
background-color: var(--bg-multiselect-color);
|
background-color: var(--bg-multiselect-color);
|
||||||
border: 1px solid var(--border-multiselect);
|
border: 1px solid var(--border-multiselect);
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root :global .selector__option {
|
.root :global .selector__option {
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Column } from 'react-table';
|
||||||
|
|
||||||
|
import type { DockerContainer } from '@/react/docker/containers/types';
|
||||||
|
|
||||||
|
export const gpus: Column<DockerContainer> = {
|
||||||
|
Header: 'GPUs',
|
||||||
|
accessor: 'Gpus',
|
||||||
|
id: 'gpus',
|
||||||
|
disableFilters: true,
|
||||||
|
canHide: true,
|
||||||
|
Filter: () => null,
|
||||||
|
};
|
|
@ -10,6 +10,7 @@ import { ports } from './ports';
|
||||||
import { quickActions } from './quick-actions';
|
import { quickActions } from './quick-actions';
|
||||||
import { stack } from './stack';
|
import { stack } from './stack';
|
||||||
import { state } from './state';
|
import { state } from './state';
|
||||||
|
import { gpus } from './gpus';
|
||||||
|
|
||||||
export function useColumns() {
|
export function useColumns() {
|
||||||
return useMemo(
|
return useMemo(
|
||||||
|
@ -22,6 +23,7 @@ export function useColumns() {
|
||||||
created,
|
created,
|
||||||
ip,
|
ip,
|
||||||
host,
|
host,
|
||||||
|
gpus,
|
||||||
ports,
|
ports,
|
||||||
ownership,
|
ownership,
|
||||||
],
|
],
|
||||||
|
|
|
@ -50,4 +50,5 @@ export type DockerContainer = {
|
||||||
Ports: Port[];
|
Ports: Port[];
|
||||||
StackName?: string;
|
StackName?: string;
|
||||||
Image: string;
|
Image: string;
|
||||||
|
Gpus: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Field, Form, Formik } from 'formik';
|
||||||
import { useReducer } from 'react';
|
import { useReducer } from 'react';
|
||||||
|
|
||||||
import { useCreateRemoteEnvironmentMutation } from '@/portainer/environments/queries/useCreateEnvironmentMutation';
|
import { useCreateRemoteEnvironmentMutation } from '@/portainer/environments/queries/useCreateEnvironmentMutation';
|
||||||
|
import { Hardware } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/Hardware';
|
||||||
import { notifySuccess } from '@/portainer/services/notifications';
|
import { notifySuccess } from '@/portainer/services/notifications';
|
||||||
import {
|
import {
|
||||||
Environment,
|
Environment,
|
||||||
|
@ -33,6 +34,7 @@ export function APIForm({ onCreate }: Props) {
|
||||||
groupId: 1,
|
groupId: 1,
|
||||||
tagIds: [],
|
tagIds: [],
|
||||||
},
|
},
|
||||||
|
gpus: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const mutation = useCreateRemoteEnvironmentMutation(
|
const mutation = useCreateRemoteEnvironmentMutation(
|
||||||
|
@ -67,7 +69,9 @@ export function APIForm({ onCreate }: Props) {
|
||||||
|
|
||||||
<TLSFieldset />
|
<TLSFieldset />
|
||||||
|
|
||||||
<MoreSettingsSection />
|
<MoreSettingsSection>
|
||||||
|
<Hardware />
|
||||||
|
</MoreSettingsSection>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div className="col-sm-12">
|
<div className="col-sm-12">
|
||||||
|
@ -96,6 +100,7 @@ export function APIForm({ onCreate }: Props) {
|
||||||
options: {
|
options: {
|
||||||
tls,
|
tls,
|
||||||
meta: values.meta,
|
meta: values.meta,
|
||||||
|
gpus: values.gpus,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { boolean, object, SchemaOf, string } from 'yup';
|
import { boolean, object, SchemaOf, string } from 'yup';
|
||||||
|
|
||||||
|
import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
||||||
|
|
||||||
import { metadataValidation } from '../../shared/MetadataFieldset/validation';
|
import { metadataValidation } from '../../shared/MetadataFieldset/validation';
|
||||||
import { nameValidation } from '../../shared/NameField';
|
import { nameValidation } from '../../shared/NameField';
|
||||||
|
|
||||||
|
@ -14,5 +16,6 @@ export function validation(): SchemaOf<FormValues> {
|
||||||
skipVerify: boolean(),
|
skipVerify: boolean(),
|
||||||
meta: metadataValidation(),
|
meta: metadataValidation(),
|
||||||
...certsValidation(),
|
...certsValidation(),
|
||||||
|
gpus: gpusListValidation(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
||||||
import { EnvironmentMetadata } from '@/portainer/environments/environment.service/create';
|
import { EnvironmentMetadata } from '@/portainer/environments/environment.service/create';
|
||||||
|
|
||||||
export interface FormValues {
|
export interface FormValues {
|
||||||
|
@ -9,4 +10,5 @@ export interface FormValues {
|
||||||
certFile?: File;
|
certFile?: File;
|
||||||
keyFile?: File;
|
keyFile?: File;
|
||||||
meta: EnvironmentMetadata;
|
meta: EnvironmentMetadata;
|
||||||
|
gpus?: Gpu[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Field, Form, Formik, useFormikContext } from 'formik';
|
||||||
import { useReducer } from 'react';
|
import { useReducer } from 'react';
|
||||||
|
|
||||||
import { useCreateLocalDockerEnvironmentMutation } from '@/portainer/environments/queries/useCreateEnvironmentMutation';
|
import { useCreateLocalDockerEnvironmentMutation } from '@/portainer/environments/queries/useCreateEnvironmentMutation';
|
||||||
|
import { Hardware } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/Hardware';
|
||||||
import { notifySuccess } from '@/portainer/services/notifications';
|
import { notifySuccess } from '@/portainer/services/notifications';
|
||||||
import { Environment } from '@/portainer/environments/types';
|
import { Environment } from '@/portainer/environments/types';
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@ export function SocketForm({ onCreate }: Props) {
|
||||||
socketPath: '',
|
socketPath: '',
|
||||||
overridePath: false,
|
overridePath: false,
|
||||||
meta: { groupId: 1, tagIds: [] },
|
meta: { groupId: 1, tagIds: [] },
|
||||||
|
gpus: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const mutation = useCreateLocalDockerEnvironmentMutation();
|
const mutation = useCreateLocalDockerEnvironmentMutation();
|
||||||
|
@ -45,7 +47,10 @@ export function SocketForm({ onCreate }: Props) {
|
||||||
|
|
||||||
<OverrideSocketFieldset />
|
<OverrideSocketFieldset />
|
||||||
|
|
||||||
<MoreSettingsSection />
|
<MoreSettingsSection>
|
||||||
|
<Hardware />
|
||||||
|
</MoreSettingsSection>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div className="col-sm-12">
|
<div className="col-sm-12">
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
|
@ -68,6 +73,7 @@ export function SocketForm({ onCreate }: Props) {
|
||||||
{
|
{
|
||||||
name: values.name,
|
name: values.name,
|
||||||
socketPath: values.overridePath ? values.socketPath : '',
|
socketPath: values.overridePath ? values.socketPath : '',
|
||||||
|
gpus: values.gpus,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess(environment) {
|
onSuccess(environment) {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { boolean, object, SchemaOf, string } from 'yup';
|
import { boolean, object, SchemaOf, string } from 'yup';
|
||||||
|
|
||||||
|
import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
||||||
|
|
||||||
import { metadataValidation } from '../../shared/MetadataFieldset/validation';
|
import { metadataValidation } from '../../shared/MetadataFieldset/validation';
|
||||||
import { nameValidation } from '../../shared/NameField';
|
import { nameValidation } from '../../shared/NameField';
|
||||||
|
|
||||||
|
@ -19,5 +21,6 @@ export function validation(): SchemaOf<FormValues> {
|
||||||
)
|
)
|
||||||
: schema
|
: schema
|
||||||
),
|
),
|
||||||
|
gpus: gpusListValidation(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
||||||
import { EnvironmentMetadata } from '@/portainer/environments/environment.service/create';
|
import { EnvironmentMetadata } from '@/portainer/environments/environment.service/create';
|
||||||
|
|
||||||
export interface FormValues {
|
export interface FormValues {
|
||||||
|
@ -5,4 +6,5 @@ export interface FormValues {
|
||||||
socketPath: string;
|
socketPath: string;
|
||||||
overridePath: boolean;
|
overridePath: boolean;
|
||||||
meta: EnvironmentMetadata;
|
meta: EnvironmentMetadata;
|
||||||
|
gpus: Gpu[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,14 @@ import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||||
|
|
||||||
import { NameField } from '../NameField';
|
import { NameField } from '../NameField';
|
||||||
import { MoreSettingsSection } from '../MoreSettingsSection';
|
import { MoreSettingsSection } from '../MoreSettingsSection';
|
||||||
|
import { Hardware } from '../Hardware/Hardware';
|
||||||
|
|
||||||
import { EnvironmentUrlField } from './EnvironmentUrlField';
|
import { EnvironmentUrlField } from './EnvironmentUrlField';
|
||||||
import { validation } from './AgentForm.validation';
|
import { validation } from './AgentForm.validation';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onCreate(environment: Environment): void;
|
onCreate(environment: Environment): void;
|
||||||
|
showGpus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialValues: CreateAgentEnvironmentValues = {
|
const initialValues: CreateAgentEnvironmentValues = {
|
||||||
|
@ -25,9 +27,10 @@ const initialValues: CreateAgentEnvironmentValues = {
|
||||||
groupId: 1,
|
groupId: 1,
|
||||||
tagIds: [],
|
tagIds: [],
|
||||||
},
|
},
|
||||||
|
gpus: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AgentForm({ onCreate }: Props) {
|
export function AgentForm({ onCreate, showGpus = false }: Props) {
|
||||||
const [formKey, clearForm] = useReducer((state) => state + 1, 0);
|
const [formKey, clearForm] = useReducer((state) => state + 1, 0);
|
||||||
|
|
||||||
const mutation = useCreateAgentEnvironmentMutation();
|
const mutation = useCreateAgentEnvironmentMutation();
|
||||||
|
@ -45,7 +48,7 @@ export function AgentForm({ onCreate }: Props) {
|
||||||
<NameField />
|
<NameField />
|
||||||
<EnvironmentUrlField />
|
<EnvironmentUrlField />
|
||||||
|
|
||||||
<MoreSettingsSection />
|
<MoreSettingsSection>{showGpus && <Hardware />}</MoreSettingsSection>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div className="col-sm-12">
|
<div className="col-sm-12">
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { object, SchemaOf, string } from 'yup';
|
import { object, SchemaOf, string } from 'yup';
|
||||||
|
|
||||||
|
import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
||||||
import { CreateAgentEnvironmentValues } from '@/portainer/environments/environment.service/create';
|
import { CreateAgentEnvironmentValues } from '@/portainer/environments/environment.service/create';
|
||||||
|
|
||||||
import { metadataValidation } from '../MetadataFieldset/validation';
|
import { metadataValidation } from '../MetadataFieldset/validation';
|
||||||
|
@ -10,5 +11,6 @@ export function validation(): SchemaOf<CreateAgentEnvironmentValues> {
|
||||||
name: nameValidation(),
|
name: nameValidation(),
|
||||||
environmentUrl: string().required('This field is required.'),
|
environmentUrl: string().required('This field is required.'),
|
||||||
meta: metadataValidation(),
|
meta: metadataValidation(),
|
||||||
|
gpus: gpusListValidation(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { FormSection } from '@@/form-components/FormSection';
|
||||||
import { LoadingButton } from '@@/buttons/LoadingButton';
|
import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||||
|
|
||||||
import { MoreSettingsSection } from '../../MoreSettingsSection';
|
import { MoreSettingsSection } from '../../MoreSettingsSection';
|
||||||
|
import { Hardware } from '../../Hardware/Hardware';
|
||||||
|
|
||||||
import { EdgeAgentFieldset } from './EdgeAgentFieldset';
|
import { EdgeAgentFieldset } from './EdgeAgentFieldset';
|
||||||
import { validationSchema } from './EdgeAgentForm.validation';
|
import { validationSchema } from './EdgeAgentForm.validation';
|
||||||
|
@ -17,11 +18,12 @@ import { FormValues } from './types';
|
||||||
interface Props {
|
interface Props {
|
||||||
onCreate(environment: Environment): void;
|
onCreate(environment: Environment): void;
|
||||||
readonly: boolean;
|
readonly: boolean;
|
||||||
|
showGpus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialValues = buildInitialValues();
|
const initialValues = buildInitialValues();
|
||||||
|
|
||||||
export function EdgeAgentForm({ onCreate, readonly }: Props) {
|
export function EdgeAgentForm({ onCreate, readonly, showGpus = false }: Props) {
|
||||||
const createMutation = useCreateEdgeAgentEnvironmentMutation();
|
const createMutation = useCreateEdgeAgentEnvironmentMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -43,6 +45,7 @@ export function EdgeAgentForm({ onCreate, readonly }: Props) {
|
||||||
value={values.pollFrequency}
|
value={values.pollFrequency}
|
||||||
/>
|
/>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
{showGpus && <Hardware />}
|
||||||
</MoreSettingsSection>
|
</MoreSettingsSection>
|
||||||
|
|
||||||
{!readonly && (
|
{!readonly && (
|
||||||
|
@ -82,6 +85,7 @@ export function buildInitialValues(): FormValues {
|
||||||
groupId: 1,
|
groupId: 1,
|
||||||
tagIds: [],
|
tagIds: [],
|
||||||
},
|
},
|
||||||
|
gpus: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
function defaultPortainerUrl() {
|
function defaultPortainerUrl() {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { number, object, SchemaOf } from 'yup';
|
import { number, object, SchemaOf } from 'yup';
|
||||||
|
|
||||||
|
import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
||||||
|
|
||||||
import { metadataValidation } from '../../MetadataFieldset/validation';
|
import { metadataValidation } from '../../MetadataFieldset/validation';
|
||||||
import { nameValidation } from '../../NameField';
|
import { nameValidation } from '../../NameField';
|
||||||
|
|
||||||
|
@ -12,5 +14,6 @@ export function validationSchema(): SchemaOf<FormValues> {
|
||||||
portainerUrl: urlValidation(),
|
portainerUrl: urlValidation(),
|
||||||
pollFrequency: number().required(),
|
pollFrequency: number().required(),
|
||||||
meta: metadataValidation(),
|
meta: metadataValidation(),
|
||||||
|
gpus: gpusListValidation(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
||||||
import { EnvironmentMetadata } from '@/portainer/environments/environment.service/create';
|
import { EnvironmentMetadata } from '@/portainer/environments/environment.service/create';
|
||||||
|
|
||||||
export interface FormValues {
|
export interface FormValues {
|
||||||
|
@ -6,4 +7,5 @@ export interface FormValues {
|
||||||
portainerUrl: string;
|
portainerUrl: string;
|
||||||
pollFrequency: number;
|
pollFrequency: number;
|
||||||
meta: EnvironmentMetadata;
|
meta: EnvironmentMetadata;
|
||||||
|
gpus: Gpu[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,12 +14,14 @@ interface Props {
|
||||||
onCreate: (environment: Environment) => void;
|
onCreate: (environment: Environment) => void;
|
||||||
commands: CommandTab[] | Partial<Record<OS, CommandTab[]>>;
|
commands: CommandTab[] | Partial<Record<OS, CommandTab[]>>;
|
||||||
isNomadTokenVisible?: boolean;
|
isNomadTokenVisible?: boolean;
|
||||||
|
showGpus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EdgeAgentTab({
|
export function EdgeAgentTab({
|
||||||
onCreate,
|
onCreate,
|
||||||
commands,
|
commands,
|
||||||
isNomadTokenVisible,
|
isNomadTokenVisible,
|
||||||
|
showGpus = false,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [edgeInfo, setEdgeInfo] = useState<EdgeInfo>();
|
const [edgeInfo, setEdgeInfo] = useState<EdgeInfo>();
|
||||||
|
|
||||||
|
@ -31,6 +33,7 @@ export function EdgeAgentTab({
|
||||||
onCreate={handleCreate}
|
onCreate={handleCreate}
|
||||||
readonly={!!edgeInfo}
|
readonly={!!edgeInfo}
|
||||||
key={formKey}
|
key={formKey}
|
||||||
|
showGpus={showGpus}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{edgeInfo && (
|
{edgeInfo && (
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { array, object, string } from 'yup';
|
||||||
|
|
||||||
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
|
|
||||||
|
import { InputList } from '@@/form-components/InputList';
|
||||||
|
import { ItemProps } from '@@/form-components/InputList/InputList';
|
||||||
|
import { InputGroup } from '@@/form-components/InputGroup';
|
||||||
|
|
||||||
|
export interface Gpu {
|
||||||
|
value: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: Gpu[];
|
||||||
|
onChange(value: Gpu[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Item({ item, onChange }: ItemProps<Gpu>) {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2 flex-grow">
|
||||||
|
<InputGroup size="small" className="flex-grow">
|
||||||
|
<InputGroup.Addon>GPU Name</InputGroup.Addon>
|
||||||
|
<InputGroup.Input
|
||||||
|
placeholder="my-gpu"
|
||||||
|
value={item.name}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange({ ...item, name: e.target.value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
<InputGroup size="small" className="flex-grow">
|
||||||
|
<InputGroup.Addon>Index or UUID</InputGroup.Addon>
|
||||||
|
<InputGroup.Input
|
||||||
|
placeholder="0 or GPU-6e2c7185-c3d3-ae22-da43-bc5267b89061"
|
||||||
|
value={item.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange({ ...item, value: e.target.value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GpusList({ value, onChange }: Props) {
|
||||||
|
return (
|
||||||
|
<InputList<Gpu>
|
||||||
|
label="GPU"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
itemBuilder={() => ({ value: '', name: '' })}
|
||||||
|
addLabel="add"
|
||||||
|
item={Item}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gpusListValidation() {
|
||||||
|
const gpuShape = object().shape({
|
||||||
|
name: string().required(),
|
||||||
|
value: string().required(),
|
||||||
|
});
|
||||||
|
return array().of(gpuShape).default([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GpusListAngular = r2a(GpusList, ['value', 'onChange']);
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { useField } from 'formik';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Gpu,
|
||||||
|
GpusList,
|
||||||
|
} from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
||||||
|
|
||||||
|
import { FormSection } from '@@/form-components/FormSection';
|
||||||
|
|
||||||
|
export function Hardware() {
|
||||||
|
const [field, , helpers] = useField('gpus');
|
||||||
|
|
||||||
|
function onChange(value: Gpu[]) {
|
||||||
|
helpers.setValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormSection title="Hardware acceleration">
|
||||||
|
<GpusList value={field.value} onChange={onChange} />
|
||||||
|
</FormSection>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue