Compare commits
25 Commits
feat/suppo
...
feat/INT-3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d85d5e272d | ||
|
|
f8c1f6ee11 | ||
|
|
e1f7411926 | ||
|
|
2fcd238320 | ||
|
|
24b5fce26d | ||
|
|
ea49a192da | ||
|
|
11e486019a | ||
|
|
1af028df4f | ||
|
|
a84ec025e8 | ||
|
|
6dfe8ad97a | ||
|
|
a6d9e566ba | ||
|
|
867168cac7 | ||
|
|
184db846c2 | ||
|
|
738ec4316d | ||
|
|
8567c4051a | ||
|
|
415af981f8 | ||
|
|
3acaee1489 | ||
|
|
27ced894fd | ||
|
|
cdf954a5e5 | ||
|
|
dbe17b9425 | ||
|
|
b36a0ec258 | ||
|
|
7ddea7e09e | ||
|
|
4173702662 | ||
|
|
11268e7816 | ||
|
|
e2bb76ff58 |
@@ -15,7 +15,7 @@ import (
|
||||
var errUnsupportedEnvironmentType = errors.New("Environment not supported")
|
||||
|
||||
const (
|
||||
defaultDockerRequestTimeout = 60
|
||||
defaultDockerRequestTimeout = 60 * time.Second
|
||||
dockerClientVersion = "1.37"
|
||||
)
|
||||
|
||||
@@ -33,22 +33,23 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers
|
||||
}
|
||||
}
|
||||
|
||||
// createClient is a generic function to create a Docker client based on
|
||||
// CreateClient is a generic function to create a Docker client based on
|
||||
// a specific environment(endpoint) configuration. The nodeName parameter can be used
|
||||
// with an agent enabled environment(endpoint) to target a specific node in an agent cluster.
|
||||
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeName string) (*client.Client, error) {
|
||||
// The underlying http client timeout may be specified, a default value is used otherwise.
|
||||
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeName string, timeout *time.Duration) (*client.Client, error) {
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
return nil, errUnsupportedEnvironmentType
|
||||
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||
return createAgentClient(endpoint, factory.signatureService, nodeName)
|
||||
return createAgentClient(endpoint, factory.signatureService, nodeName, timeout)
|
||||
} else if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
|
||||
return createEdgeClient(endpoint, factory.signatureService, factory.reverseTunnelService, nodeName)
|
||||
return createEdgeClient(endpoint, factory.signatureService, factory.reverseTunnelService, nodeName, timeout)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
|
||||
return createLocalClient(endpoint)
|
||||
}
|
||||
return createTCPClient(endpoint)
|
||||
return createTCPClient(endpoint, timeout)
|
||||
}
|
||||
|
||||
func createLocalClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||
@@ -58,8 +59,8 @@ func createLocalClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||
)
|
||||
}
|
||||
|
||||
func createTCPClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||
httpCli, err := httpClient(endpoint)
|
||||
func createTCPClient(endpoint *portainer.Endpoint, timeout *time.Duration) (*client.Client, error) {
|
||||
httpCli, err := httpClient(endpoint, timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -71,8 +72,8 @@ func createTCPClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||
)
|
||||
}
|
||||
|
||||
func createEdgeClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, nodeName string) (*client.Client, error) {
|
||||
httpCli, err := httpClient(endpoint)
|
||||
func createEdgeClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, nodeName string, timeout *time.Duration) (*client.Client, error) {
|
||||
httpCli, err := httpClient(endpoint, timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -95,7 +96,7 @@ func createEdgeClient(endpoint *portainer.Endpoint, signatureService portainer.D
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
endpointURL := fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
|
||||
|
||||
return client.NewClientWithOpts(
|
||||
@@ -106,8 +107,8 @@ func createEdgeClient(endpoint *portainer.Endpoint, signatureService portainer.D
|
||||
)
|
||||
}
|
||||
|
||||
func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService, nodeName string) (*client.Client, error) {
|
||||
httpCli, err := httpClient(endpoint)
|
||||
func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService, nodeName string, timeout *time.Duration) (*client.Client, error) {
|
||||
httpCli, err := httpClient(endpoint, timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -134,7 +135,7 @@ func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.
|
||||
)
|
||||
}
|
||||
|
||||
func httpClient(endpoint *portainer.Endpoint) (*http.Client, error) {
|
||||
func httpClient(endpoint *portainer.Endpoint, timeout *time.Duration) (*http.Client, error) {
|
||||
transport := &http.Transport{}
|
||||
|
||||
if endpoint.TLSConfig.TLS {
|
||||
@@ -145,8 +146,13 @@ func httpClient(endpoint *portainer.Endpoint) (*http.Client, error) {
|
||||
transport.TLSClientConfig = tlsConfig
|
||||
}
|
||||
|
||||
clientTimeout := defaultDockerRequestTimeout
|
||||
if timeout != nil {
|
||||
clientTimeout = *timeout
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: defaultDockerRequestTimeout * time.Second,
|
||||
Timeout: clientTimeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func NewSnapshotter(clientFactory *ClientFactory) *Snapshotter {
|
||||
|
||||
// CreateSnapshot creates a snapshot of a specific Docker environment(endpoint)
|
||||
func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*portainer.DockerSnapshot, error) {
|
||||
cli, err := snapshotter.clientFactory.CreateClient(endpoint, "")
|
||||
cli, err := snapshotter.clientFactory.CreateClient(endpoint, "", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
101
api/fdo/ownerclient/aux.go
Normal file
101
api/fdo/ownerclient/aux.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package ownerclient
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (c FDOOwnerClient) GetVouchers() ([]string, error) {
|
||||
resp, err := c.doDigestAuthReq(
|
||||
http.MethodGet,
|
||||
"api/v1/owner/vouchers",
|
||||
"",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New(http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
contents, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
guids := strings.FieldsFunc(
|
||||
strings.TrimSpace(string(contents)),
|
||||
func(c rune) bool {
|
||||
return c == ','
|
||||
},
|
||||
)
|
||||
|
||||
return guids, nil
|
||||
}
|
||||
|
||||
func (c FDOOwnerClient) DeleteVoucher(guid string) error {
|
||||
resp, err := c.doDigestAuthReq(
|
||||
http.MethodDelete,
|
||||
"api/v1/owner/vouchers?id="+guid,
|
||||
"",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c FDOOwnerClient) GetDeviceSVI(guid string) (string, error) {
|
||||
resp, err := c.doDigestAuthReq(
|
||||
http.MethodGet,
|
||||
"api/v1/device/svi?guid="+guid,
|
||||
"",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", errors.New(http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func (c FDOOwnerClient) DeleteDeviceSVI(id string) error {
|
||||
resp, err := c.doDigestAuthReq(
|
||||
http.MethodDelete,
|
||||
"api/v1/device/svi?id="+id,
|
||||
"",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
142
api/fdo/ownerclient/client.go
Normal file
142
api/fdo/ownerclient/client.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package ownerclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rkl-/digest"
|
||||
)
|
||||
|
||||
type FDOOwnerClient struct {
|
||||
OwnerURL string
|
||||
Username string
|
||||
Password string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
type ServiceInfo struct {
|
||||
Module string
|
||||
Var string
|
||||
Filename string
|
||||
Bytes []byte
|
||||
GUID string
|
||||
Device string
|
||||
Priority int
|
||||
OS string
|
||||
Version string
|
||||
Arch string
|
||||
CRID int
|
||||
Hash string
|
||||
}
|
||||
|
||||
func (c FDOOwnerClient) doDigestAuthReq(method, endpoint, contentType string, body io.Reader) (*http.Response, error) {
|
||||
transport := digest.NewTransport(c.Username, c.Password)
|
||||
|
||||
client, err := transport.Client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client.Timeout = c.Timeout
|
||||
|
||||
e, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u, err := url.Parse(c.OwnerURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, u.ResolveReference(e).String(), body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
func (c FDOOwnerClient) PostVoucher(ov []byte) (string, error) {
|
||||
resp, err := c.doDigestAuthReq(
|
||||
http.MethodPost,
|
||||
"api/v1/owner/vouchers",
|
||||
"application/cbor",
|
||||
bytes.NewReader(ov),
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", errors.New(http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func (c FDOOwnerClient) PutDeviceSVI(info ServiceInfo) error {
|
||||
values := url.Values{}
|
||||
values.Set("module", info.Module)
|
||||
values.Set("var", info.Var)
|
||||
values.Set("filename", info.Filename)
|
||||
values.Set("guid", info.GUID)
|
||||
values.Set("device", info.Device)
|
||||
values.Set("priority", strconv.Itoa(info.Priority))
|
||||
values.Set("os", info.OS)
|
||||
values.Set("version", info.Version)
|
||||
values.Set("arch", info.Arch)
|
||||
values.Set("crid", strconv.Itoa(info.CRID))
|
||||
values.Set("hash", info.Hash)
|
||||
|
||||
resp, err := c.doDigestAuthReq(
|
||||
http.MethodPut,
|
||||
"api/v1/device/svi?"+values.Encode(),
|
||||
"application/octet-stream",
|
||||
strings.NewReader(string(info.Bytes)),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c FDOOwnerClient) PutDeviceSVIRaw(info url.Values, body []byte) error {
|
||||
resp, err := c.doDigestAuthReq(
|
||||
http.MethodPut,
|
||||
"api/v1/device/svi?"+info.Encode(),
|
||||
"application/octet-stream",
|
||||
strings.NewReader(string(body)),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -17,6 +17,7 @@ require (
|
||||
github.com/docker/cli v20.10.9+incompatible
|
||||
github.com/docker/docker v20.10.9+incompatible
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.3.0
|
||||
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
|
||||
github.com/go-git/go-git/v5 v5.3.0
|
||||
github.com/go-ldap/ldap/v3 v3.1.8
|
||||
@@ -39,6 +40,7 @@ require (
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
|
||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
|
||||
github.com/portainer/libhttp v0.0.0-20211021135806-13e6c55c5fbc
|
||||
github.com/rkl-/digest v0.0.0-20180419075440-8316caa4a777
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
|
||||
@@ -300,6 +300,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
|
||||
github.com/fxamacker/cbor/v2 v2.3.0 h1:aM45YGMctNakddNNAezPxDUpv38j44Abh+hifNuqXik=
|
||||
github.com/fxamacker/cbor/v2 v2.3.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
|
||||
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814 h1:gWvniJ4GbFfkf700kykAImbLiEMU0Q3QN9hQ26Js1pU=
|
||||
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814/go.mod h1:secRm32Ro77eD23BmPVbgLbWN+JWDw7pJszenjxI4bI=
|
||||
github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
||||
@@ -649,6 +651,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
|
||||
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rkl-/digest v0.0.0-20180419075440-8316caa4a777 h1:rDj3WeO+TiWyxfcydUnKegWAZoR5kQsnW0wzhggdOrw=
|
||||
github.com/rkl-/digest v0.0.0-20180419075440-8316caa4a777/go.mod h1:xRVvTK+cS/dJSvrOufGUQFWfgvE7yXExeng96n8377o=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
@@ -722,6 +726,8 @@ github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
|
||||
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
|
||||
|
||||
@@ -14,7 +14,7 @@ type authenticationResponse struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func (service *Service) executeAuthenticationRequest(configuration portainer.OpenAMTConfiguration) (*authenticationResponse, error) {
|
||||
func (service *Service) Authorization(configuration portainer.OpenAMTConfiguration) (string, error) {
|
||||
loginURL := fmt.Sprintf("https://%s/mps/login/api/v1/authorize", configuration.MPSServer)
|
||||
|
||||
payload := map[string]string{
|
||||
@@ -25,28 +25,28 @@ func (service *Service) executeAuthenticationRequest(configuration portainer.Ope
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, loginURL, bytes.NewBuffer(jsonValue))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
response, err := service.httpsClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
responseBody, readErr := ioutil.ReadAll(response.Body)
|
||||
if readErr != nil {
|
||||
return nil, readErr
|
||||
return "", readErr
|
||||
}
|
||||
errorResponse := parseError(responseBody)
|
||||
if errorResponse != nil {
|
||||
return nil, errorResponse
|
||||
return "", errorResponse
|
||||
}
|
||||
|
||||
var token authenticationResponse
|
||||
err = json.Unmarshal(responseBody, &token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
return &token, nil
|
||||
return token.Token, nil
|
||||
}
|
||||
|
||||
87
api/hostmanagement/openamt/configDevice.go
Normal file
87
api/hostmanagement/openamt/configDevice.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package openamt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
GUID string `json:"guid"`
|
||||
HostName string `json:"hostname"`
|
||||
ConnectionStatus bool `json:"connectionStatus"`
|
||||
}
|
||||
|
||||
type DevicePowerState struct {
|
||||
State portainer.PowerState `json:"powerstate"`
|
||||
}
|
||||
|
||||
type DeviceEnabledFeatures struct {
|
||||
Redirection bool `json:"redirection"`
|
||||
KVM bool `json:"KVM"`
|
||||
SOL bool `json:"SOL"`
|
||||
IDER bool `json:"IDER"`
|
||||
UserConsent string `json:"userConsent"`
|
||||
}
|
||||
|
||||
func (service *Service) getDevice(configuration portainer.OpenAMTConfiguration, deviceGUID string) (*Device, error) {
|
||||
url := fmt.Sprintf("https://%s/mps/api/v1/devices/%s", configuration.MPSServer, deviceGUID)
|
||||
|
||||
responseBody, err := service.executeGetRequest(url, configuration.Credentials.MPSToken)
|
||||
if err != nil {
|
||||
if strings.EqualFold(err.Error(), "invalid value") {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if responseBody == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var result Device
|
||||
err = json.Unmarshal(responseBody, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (service *Service) getDevicePowerState(configuration portainer.OpenAMTConfiguration, deviceGUID string) (*DevicePowerState, error) {
|
||||
url := fmt.Sprintf("https://%s/mps/api/v1/amt/power/state/%s", configuration.MPSServer, deviceGUID)
|
||||
|
||||
responseBody, err := service.executeGetRequest(url, configuration.Credentials.MPSToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if responseBody == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var result DevicePowerState
|
||||
err = json.Unmarshal(responseBody, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (service *Service) getDeviceEnabledFeatures(configuration portainer.OpenAMTConfiguration, deviceGUID string) (*DeviceEnabledFeatures, error) {
|
||||
url := fmt.Sprintf("https://%s/mps/api/v1/amt/features/%s", configuration.MPSServer, deviceGUID)
|
||||
|
||||
responseBody, err := service.executeGetRequest(url, configuration.Credentials.MPSToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if responseBody == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var result DeviceEnabledFeatures
|
||||
err = json.Unmarshal(responseBody, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -29,7 +29,7 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
func (service *Service) createOrUpdateAMTProfile(configuration portainer.OpenAMTConfiguration, profileName string, ciraConfigName string, wirelessConfig string) (*Profile, error) {
|
||||
func (service *Service) createOrUpdateAMTProfile(configuration portainer.OpenAMTConfiguration, profileName string, ciraConfigName string) (*Profile, error) {
|
||||
profile, err := service.getAMTProfile(configuration, profileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -40,7 +40,7 @@ func (service *Service) createOrUpdateAMTProfile(configuration portainer.OpenAMT
|
||||
method = http.MethodPatch
|
||||
}
|
||||
|
||||
profile, err = service.saveAMTProfile(method, configuration, profileName, ciraConfigName, wirelessConfig)
|
||||
profile, err = service.saveAMTProfile(method, configuration, profileName, ciraConfigName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -66,7 +66,7 @@ func (service *Service) getAMTProfile(configuration portainer.OpenAMTConfigurati
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (service *Service) saveAMTProfile(method string, configuration portainer.OpenAMTConfiguration, profileName string, ciraConfigName string, wirelessConfig string) (*Profile, error) {
|
||||
func (service *Service) saveAMTProfile(method string, configuration portainer.OpenAMTConfiguration, profileName string, ciraConfigName string) (*Profile, error) {
|
||||
url := fmt.Sprintf("https://%s/rps/api/v1/admin/profiles", configuration.MPSServer)
|
||||
|
||||
profile := Profile{
|
||||
@@ -80,14 +80,6 @@ func (service *Service) saveAMTProfile(method string, configuration portainer.Op
|
||||
Tags: []string{},
|
||||
DHCPEnabled: true,
|
||||
}
|
||||
if wirelessConfig != "" {
|
||||
profile.WIFIConfigs = []ProfileWifiConfig{
|
||||
{
|
||||
Priority: 1,
|
||||
ProfileName: DefaultWirelessConfigName,
|
||||
},
|
||||
}
|
||||
}
|
||||
payload, _ := json.Marshal(profile)
|
||||
|
||||
responseBody, err := service.executeSaveRequest(method, url, configuration.Credentials.MPSToken, payload)
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
package openamt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type (
|
||||
WirelessProfile struct {
|
||||
ProfileName string `json:"profileName"`
|
||||
AuthenticationMethod int `json:"authenticationMethod"`
|
||||
EncryptionMethod int `json:"encryptionMethod"`
|
||||
SSID string `json:"ssid"`
|
||||
PSKPassphrase string `json:"pskPassphrase"`
|
||||
}
|
||||
)
|
||||
|
||||
func (service *Service) createOrUpdateWirelessConfig(configuration portainer.OpenAMTConfiguration, wirelessConfigName string) (*WirelessProfile, error) {
|
||||
wirelessConfig, err := service.getWirelessConfig(configuration, wirelessConfigName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
method := http.MethodPost
|
||||
if wirelessConfig != nil {
|
||||
method = http.MethodPatch
|
||||
}
|
||||
|
||||
wirelessConfig, err = service.saveWirelessConfig(method, configuration, wirelessConfigName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wirelessConfig, nil
|
||||
}
|
||||
|
||||
func (service *Service) getWirelessConfig(configuration portainer.OpenAMTConfiguration, configName string) (*WirelessProfile, error) {
|
||||
url := fmt.Sprintf("https://%s/rps/api/v1/admin/wirelessconfigs/%s", configuration.MPSServer, configName)
|
||||
|
||||
responseBody, err := service.executeGetRequest(url, configuration.Credentials.MPSToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if responseBody == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var result WirelessProfile
|
||||
err = json.Unmarshal(responseBody, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (service *Service) saveWirelessConfig(method string, configuration portainer.OpenAMTConfiguration, configName string) (*WirelessProfile, error) {
|
||||
parsedAuthenticationMethod, err := strconv.Atoi(configuration.WirelessConfiguration.AuthenticationMethod)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing wireless authentication method: %s", err.Error())
|
||||
}
|
||||
parsedEncryptionMethod, err := strconv.Atoi(configuration.WirelessConfiguration.EncryptionMethod)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing wireless encryption method: %s", err.Error())
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://%s/rps/api/v1/admin/wirelessconfigs", configuration.MPSServer)
|
||||
|
||||
config := WirelessProfile{
|
||||
ProfileName: configName,
|
||||
AuthenticationMethod: parsedAuthenticationMethod,
|
||||
EncryptionMethod: parsedEncryptionMethod,
|
||||
SSID: configuration.WirelessConfiguration.SSID,
|
||||
PSKPassphrase: configuration.WirelessConfiguration.PskPass,
|
||||
}
|
||||
payload, _ := json.Marshal(config)
|
||||
|
||||
responseBody, err := service.executeSaveRequest(method, url, configuration.Credentials.MPSToken, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result WirelessProfile
|
||||
err = json.Unmarshal(responseBody, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
55
api/hostmanagement/openamt/deviceActions.go
Normal file
55
api/hostmanagement/openamt/deviceActions.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package openamt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type ActionResponse struct {
|
||||
Body struct {
|
||||
ReturnValue int `json:"ReturnValue"`
|
||||
ReturnValueStr string `json:"ReturnValueStr"`
|
||||
} `json:"Body"`
|
||||
}
|
||||
|
||||
func (service *Service) executeDeviceAction(configuration portainer.OpenAMTConfiguration, deviceGUID string, action int) error {
|
||||
url := fmt.Sprintf("https://%s/mps/api/v1/amt/power/action/%s", configuration.MPSServer, deviceGUID)
|
||||
|
||||
payload := map[string]int{
|
||||
"action": action,
|
||||
}
|
||||
jsonValue, _ := json.Marshal(payload)
|
||||
|
||||
responseBody, err := service.executeSaveRequest(http.MethodPost, url, configuration.Credentials.MPSToken, jsonValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var response ActionResponse
|
||||
err = json.Unmarshal(responseBody, &response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if response.Body.ReturnValue != 0 {
|
||||
return fmt.Errorf("failed to execute action, error status %v: %s", response.Body.ReturnValue, response.Body.ReturnValueStr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseAction(actionRaw string) (portainer.PowerState, error) {
|
||||
switch strings.ToLower(actionRaw) {
|
||||
case "power up":
|
||||
return powerUpState, nil
|
||||
case "power off":
|
||||
return powerOffState, nil
|
||||
case "restart":
|
||||
return restartState, nil
|
||||
}
|
||||
return 0, fmt.Errorf("unsupported device action %s", actionRaw)
|
||||
}
|
||||
29
api/hostmanagement/openamt/deviceFeatures.go
Normal file
29
api/hostmanagement/openamt/deviceFeatures.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package openamt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func (service *Service) enableDeviceFeatures(configuration portainer.OpenAMTConfiguration, deviceGUID string, features portainer.OpenAMTDeviceEnabledFeatures) error {
|
||||
url := fmt.Sprintf("https://%s/mps/api/v1/amt/features/%s", configuration.MPSServer, deviceGUID)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"enableSOL": features.SOL,
|
||||
"enableIDER": features.IDER,
|
||||
"enableKVM": features.KVM,
|
||||
"redirection": features.Redirection,
|
||||
"userConsent": features.UserConsent,
|
||||
}
|
||||
jsonValue, _ := json.Marshal(payload)
|
||||
|
||||
_, err := service.executeSaveRequest(http.MethodPost, url, configuration.Credentials.MPSToken, jsonValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -16,8 +17,13 @@ import (
|
||||
|
||||
const (
|
||||
DefaultCIRAConfigName = "ciraConfigDefault"
|
||||
DefaultWirelessConfigName = "wirelessProfileDefault"
|
||||
DefaultProfileName = "profileAMTDefault"
|
||||
|
||||
httpClientTimeout = 5 * time.Minute
|
||||
|
||||
powerUpState portainer.PowerState = 2
|
||||
powerOffState portainer.PowerState = 8
|
||||
restartState portainer.PowerState = 5
|
||||
)
|
||||
|
||||
// Service represents a service for managing an OpenAMT server.
|
||||
@@ -32,7 +38,7 @@ func NewService(dataStore dataservices.DataStore) *Service {
|
||||
}
|
||||
return &Service{
|
||||
httpsClient: &http.Client{
|
||||
Timeout: time.Second * time.Duration(5),
|
||||
Timeout: httpClientTimeout,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
@@ -64,27 +70,18 @@ func parseError(responseBody []byte) error {
|
||||
}
|
||||
|
||||
func (service *Service) ConfigureDefault(configuration portainer.OpenAMTConfiguration) error {
|
||||
token, err := service.executeAuthenticationRequest(configuration)
|
||||
token, err := service.Authorization(configuration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configuration.Credentials.MPSToken = token.Token
|
||||
configuration.Credentials.MPSToken = token
|
||||
|
||||
ciraConfig, err := service.createOrUpdateCIRAConfig(configuration, DefaultCIRAConfigName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wirelessConfigName := ""
|
||||
if configuration.WirelessConfiguration != nil {
|
||||
wirelessConfig, err := service.createOrUpdateWirelessConfig(configuration, DefaultWirelessConfigName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wirelessConfigName = wirelessConfig.ProfileName
|
||||
}
|
||||
|
||||
_, err = service.createOrUpdateAMTProfile(configuration, DefaultProfileName, ciraConfig.ConfigName, wirelessConfigName)
|
||||
_, err = service.createOrUpdateAMTProfile(configuration, DefaultProfileName, ciraConfig.ConfigName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -156,3 +153,117 @@ func (service *Service) executeGetRequest(url string, token string) ([]byte, err
|
||||
|
||||
return responseBody, nil
|
||||
}
|
||||
|
||||
func (service *Service) DeviceInformation(configuration portainer.OpenAMTConfiguration, deviceGUID string) (*portainer.OpenAMTDeviceInformation, error) {
|
||||
token, err := service.Authorization(configuration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configuration.Credentials.MPSToken = token
|
||||
|
||||
amtErrors := make(chan error)
|
||||
wgDone := make(chan bool)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var resultDevice *Device
|
||||
var resultPowerState *DevicePowerState
|
||||
var resultEnabledFeatures *DeviceEnabledFeatures
|
||||
wg.Add(3)
|
||||
|
||||
go func() {
|
||||
device, err := service.getDevice(configuration, deviceGUID)
|
||||
if err != nil {
|
||||
amtErrors <- err
|
||||
}
|
||||
if device == nil {
|
||||
amtErrors <- fmt.Errorf("device %s not found", deviceGUID)
|
||||
}
|
||||
resultDevice = device
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
powerState, err := service.getDevicePowerState(configuration, deviceGUID)
|
||||
if err != nil {
|
||||
amtErrors <- err
|
||||
}
|
||||
resultPowerState = powerState
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
enabledFeatures, err := service.getDeviceEnabledFeatures(configuration, deviceGUID)
|
||||
if err != nil {
|
||||
amtErrors <- err
|
||||
}
|
||||
resultEnabledFeatures = enabledFeatures
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(wgDone)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-wgDone:
|
||||
break
|
||||
case err := <-amtErrors:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deviceInformation := &portainer.OpenAMTDeviceInformation{
|
||||
GUID: resultDevice.GUID,
|
||||
HostName: resultDevice.HostName,
|
||||
ConnectionStatus: resultDevice.ConnectionStatus,
|
||||
}
|
||||
if resultPowerState != nil {
|
||||
deviceInformation.PowerState = resultPowerState.State
|
||||
}
|
||||
if resultEnabledFeatures != nil {
|
||||
deviceInformation.EnabledFeatures = &portainer.OpenAMTDeviceEnabledFeatures{
|
||||
Redirection: resultEnabledFeatures.Redirection,
|
||||
KVM: resultEnabledFeatures.KVM,
|
||||
SOL: resultEnabledFeatures.SOL,
|
||||
IDER: resultEnabledFeatures.IDER,
|
||||
UserConsent: resultEnabledFeatures.UserConsent,
|
||||
}
|
||||
}
|
||||
|
||||
return deviceInformation, nil
|
||||
}
|
||||
|
||||
func (service *Service) ExecuteDeviceAction(configuration portainer.OpenAMTConfiguration, deviceGUID string, action string) error {
|
||||
parsedAction, err := parseAction(action)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
token, err := service.Authorization(configuration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configuration.Credentials.MPSToken = token
|
||||
|
||||
err = service.executeDeviceAction(configuration, deviceGUID, int(parsedAction))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) EnableDeviceFeatures(configuration portainer.OpenAMTConfiguration, deviceGUID string, features portainer.OpenAMTDeviceEnabledFeatures) (string, error) {
|
||||
token, err := service.Authorization(configuration)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
configuration.Credentials.MPSToken = token
|
||||
|
||||
err = service.enableDeviceFeatures(configuration, deviceGUID, features)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/portainer/portainer/api/http/handler/endpoints"
|
||||
"github.com/portainer/portainer/api/http/handler/file"
|
||||
"github.com/portainer/portainer/api/http/handler/helm"
|
||||
"github.com/portainer/portainer/api/http/handler/hostmanagement/fdo"
|
||||
"github.com/portainer/portainer/api/http/handler/hostmanagement/openamt"
|
||||
"github.com/portainer/portainer/api/http/handler/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/handler/ldap"
|
||||
@@ -64,6 +65,7 @@ type Handler struct {
|
||||
SettingsHandler *settings.Handler
|
||||
SSLHandler *ssl.Handler
|
||||
OpenAMTHandler *openamt.Handler
|
||||
FDOHandler *fdo.Handler
|
||||
StackHandler *stacks.Handler
|
||||
StatusHandler *status.Handler
|
||||
StorybookHandler *storybook.Handler
|
||||
@@ -231,6 +233,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if h.OpenAMTHandler != nil {
|
||||
http.StripPrefix("/api", h.OpenAMTHandler).ServeHTTP(w, r)
|
||||
}
|
||||
case strings.HasPrefix(r.URL.Path, "/api/fdo"):
|
||||
if h.FDOHandler != nil {
|
||||
http.StripPrefix("/api", h.FDOHandler).ServeHTTP(w, r)
|
||||
}
|
||||
case strings.HasPrefix(r.URL.Path, "/api/teams"):
|
||||
http.StripPrefix("/api", h.TeamHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/team_memberships"):
|
||||
|
||||
179
api/http/handler/hostmanagement/fdo/configure.go
Normal file
179
api/http/handler/hostmanagement/fdo/configure.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package fdo
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
cbor "github.com/fxamacker/cbor/v2"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type deviceConfigurePayload struct {
|
||||
EdgeKey string `json:"edgekey"`
|
||||
Name string `json:"name"`
|
||||
Profile int `json:"profile"`
|
||||
}
|
||||
|
||||
func (payload *deviceConfigurePayload) Validate(r *http.Request) error {
|
||||
if payload.EdgeKey == "" {
|
||||
return errors.New("invalid edge key provided")
|
||||
}
|
||||
|
||||
if payload.Name == "" {
|
||||
return errors.New("the device name cannot be empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id fdoConfigureDevice
|
||||
// @summary configure an FDO device
|
||||
// @description configure an FDO device
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param body body deviceConfigurePayload true "Device Configuration"
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo/configure/{guid} [post]
|
||||
func (handler *Handler) fdoConfigureDevice(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
guid, err := request.RetrieveRouteVariableValue(r, "guid")
|
||||
if err != nil {
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: request.RetrieveRouteVariableValue()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: guid not found", Err: err}
|
||||
}
|
||||
|
||||
var payload deviceConfigurePayload
|
||||
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Invalid request payload")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
|
||||
fdoClient, err := handler.newFDOClient()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: newFDOClient()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: newFDOClient()", Err: err}
|
||||
}
|
||||
|
||||
// enable fdo_sys
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"0"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"active"},
|
||||
"bytes": []string{"F5"}, // this is "true" in CBOR
|
||||
}, []byte("")); err != nil {
|
||||
logrus.WithError(err).Info("fdoRegisterDevice: PutDeviceSVI()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoRegisterDevice: PutDeviceSVI()", Err: err}
|
||||
}
|
||||
// write down the edgekey
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"1"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"filedesc"},
|
||||
"filename": []string{"DEVICE_edgekey.txt"},
|
||||
}, []byte(payload.EdgeKey)); err != nil {
|
||||
logrus.WithError(err).Info("fdoRegisterDevice: PutDeviceSVI(edgekey)")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoRegisterDevice: PutDeviceSVI(edgekey)", Err: err}
|
||||
}
|
||||
// write down the device name
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"1"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"filedesc"},
|
||||
"filename": []string{"DEVICE_name.txt"},
|
||||
}, []byte(payload.Name)); err != nil {
|
||||
logrus.WithError(err).Info("fdoRegisterDevice: PutDeviceSVI(name)")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoRegisterDevice: PutDeviceSVI(name)", Err: err}
|
||||
}
|
||||
// write down the device GUID - used as the EDGE_DEVICE_GUID too
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"1"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"filedesc"},
|
||||
"filename": []string{"DEVICE_GUID.txt"},
|
||||
}, []byte(guid)); err != nil {
|
||||
logrus.WithError(err).Info("fdoRegisterDevice: PutDeviceSVI()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoRegisterDevice: PutDeviceSVI()", Err: err}
|
||||
}
|
||||
|
||||
// onboarding script - this would get selected by the profile name
|
||||
deploymentScriptName := "portainer.sh"
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"1"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"filedesc"},
|
||||
"filename": []string{deploymentScriptName},
|
||||
}, []byte(`#!/bin/bash -ex
|
||||
# deploying `+strconv.Itoa(payload.Profile)+`
|
||||
env > env.log
|
||||
|
||||
export AGENT_IMAGE=portainer/agent:2.9.3
|
||||
export GUID=$(cat DEVICE_GUID.txt)
|
||||
export DEVICE_NAME=$(cat DEVICE_name.txt)
|
||||
export EDGE_KEY=$(cat DEVICE_edgekey.txt)
|
||||
export AGENTVOLUME=$(pwd)/data/portainer_agent_data/
|
||||
|
||||
mkdir -p ${AGENTVOLUME}
|
||||
|
||||
docker pull ${AGENT_IMAGE}
|
||||
|
||||
docker run -d \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v /var/lib/docker/volumes:/var/lib/docker/volumes \
|
||||
-v /:/host \
|
||||
-v ${AGENTVOLUME}:/data \
|
||||
--restart always \
|
||||
-e EDGE=1 \
|
||||
-e EDGE_ID=${GUID} \
|
||||
-e EDGE_KEY=${EDGE_KEY} \
|
||||
-e CAP_HOST_MANAGEMENT=1 \
|
||||
-e EDGE_INSECURE_POLL=1 \
|
||||
--name portainer_edge_agent \
|
||||
${AGENT_IMAGE}
|
||||
`)); err != nil {
|
||||
logrus.WithError(err).Info("fdoRegisterDevice: PutDeviceSVI()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoRegisterDevice: PutDeviceSVI()", Err: err}
|
||||
}
|
||||
|
||||
b, err := cbor.Marshal([]string{"/bin/sh", deploymentScriptName})
|
||||
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("failed to marshal string to CBOR")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoRegisterDevice: PutDeviceSVI() failed to encode", Err: err}
|
||||
|
||||
}
|
||||
|
||||
cbor := strings.ToUpper(hex.EncodeToString(b))
|
||||
logrus.WithField("cbor", cbor).WithField("string", deploymentScriptName).Info("converted to CBOR")
|
||||
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"2"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"exec"},
|
||||
"bytes": []string{cbor},
|
||||
}, []byte("")); err != nil {
|
||||
logrus.WithError(err).Info("fdoRegisterDevice: PutDeviceSVI()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoRegisterDevice: PutDeviceSVI()", Err: err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
95
api/http/handler/hostmanagement/fdo/fdo.go
Normal file
95
api/http/handler/hostmanagement/fdo/fdo.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package fdo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/fdo/ownerclient"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type fdoConfigurePayload portainer.FDOConfiguration
|
||||
|
||||
func (payload *fdoConfigurePayload) Validate(r *http.Request) error {
|
||||
if payload.Enabled {
|
||||
parsedUrl, err := url.Parse(payload.OwnerURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if parsedUrl.Scheme != "http" && parsedUrl.Scheme != "https" {
|
||||
return errors.New("invalid scheme provided, must be 'http' or 'https'")
|
||||
}
|
||||
|
||||
if parsedUrl.Host == "" {
|
||||
return errors.New("invalid host provided")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) saveSettings(config portainer.FDOConfiguration) error {
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.FDOConfiguration = config
|
||||
|
||||
return handler.DataStore.Settings().UpdateSettings(settings)
|
||||
}
|
||||
|
||||
func (handler *Handler) newFDOClient() (ownerclient.FDOOwnerClient, error) {
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return ownerclient.FDOOwnerClient{}, err
|
||||
}
|
||||
|
||||
return ownerclient.FDOOwnerClient{
|
||||
OwnerURL: settings.FDOConfiguration.OwnerURL,
|
||||
Username: settings.FDOConfiguration.OwnerUsername,
|
||||
Password: settings.FDOConfiguration.OwnerPassword,
|
||||
Timeout: 5 * time.Second,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// @id fdoConfigure
|
||||
// @summary Enable Portainer's FDO capabilities
|
||||
// @description Enable Portainer's FDO capabilities
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param body body fdoConfigurePayload true "FDO Settings"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo [post]
|
||||
func (handler *Handler) fdoConfigure(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload fdoConfigurePayload
|
||||
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Invalid request payload")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
|
||||
var settings portainer.FDOConfiguration
|
||||
if payload.Enabled {
|
||||
settings = portainer.FDOConfiguration(payload)
|
||||
}
|
||||
|
||||
if err = handler.saveSettings(settings); err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Error saving FDO settings", Err: err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
35
api/http/handler/hostmanagement/fdo/handler.go
Normal file
35
api/http/handler/hostmanagement/fdo/handler.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package fdo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore dataservices.DataStore
|
||||
}
|
||||
|
||||
func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore) *Handler {
|
||||
if !dataStore.Settings().IsFeatureFlagEnabled(portainer.FeatFDO) {
|
||||
return nil
|
||||
}
|
||||
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
DataStore: dataStore,
|
||||
}
|
||||
|
||||
h.Handle("/fdo", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoConfigure))).Methods(http.MethodPost)
|
||||
h.Handle("/fdo/list", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoListAll))).Methods(http.MethodGet)
|
||||
h.Handle("/fdo/register", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoRegisterDevice))).Methods(http.MethodPost)
|
||||
h.Handle("/fdo/configure/{guid}", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoConfigureDevice))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
39
api/http/handler/hostmanagement/fdo/list.go
Normal file
39
api/http/handler/hostmanagement/fdo/list.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package fdo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
// @id fdoListAll
|
||||
// @summary List all known FDO vouchers
|
||||
// @description List all known FDO vouchers
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo/list [get]
|
||||
func (handler *Handler) fdoListAll(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
fdoClient, err := handler.newFDOClient()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Info("fdoListAll: newFDOClient()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoRegisterDevice: newFDOClient()", Err: err}
|
||||
}
|
||||
|
||||
// Get all vouchers
|
||||
guids, err := fdoClient.GetVouchers()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Info("fdoListAll: GetVouchers()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoListAll: GetVouchers()", Err: err}
|
||||
}
|
||||
|
||||
return response.JSON(w, guids)
|
||||
}
|
||||
49
api/http/handler/hostmanagement/fdo/register.go
Normal file
49
api/http/handler/hostmanagement/fdo/register.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package fdo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type registerDeviceResponse struct {
|
||||
Guid string `json:"guid" example:"c6ea3343-229a-4c07-9096-beef7134e1d3"`
|
||||
}
|
||||
|
||||
// @id fdoRegisterDevice
|
||||
// @summary register an FDO device
|
||||
// @description register an FDO device
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo/register [post]
|
||||
func (handler *Handler) fdoRegisterDevice(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
// Post a voucher
|
||||
ov, filename, err := request.RetrieveMultiPartFormFile(r, "voucher")
|
||||
if err != nil {
|
||||
logrus.WithField("filename", filename).WithError(err).Info("fdoRegisterDevice: readVoucher()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoRegisterDevice: read Voucher()", Err: err}
|
||||
}
|
||||
|
||||
fdoClient, err := handler.newFDOClient()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Info("fdoRegisterDevice: newFDOClient()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoRegisterDevice: newFDOClient()", Err: err}
|
||||
}
|
||||
|
||||
guid, err := fdoClient.PostVoucher(ov)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Info("fdoRegisterDevice: PostVoucher()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoRegisterDevice: PostVoucher()", Err: err}
|
||||
}
|
||||
|
||||
return response.JSON(w, registerDeviceResponse{guid})
|
||||
}
|
||||
73
api/http/handler/hostmanagement/openamt/amtactivation.go
Normal file
73
api/http/handler/hostmanagement/openamt/amtactivation.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package openamt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
)
|
||||
|
||||
// @id openAMTActivate
|
||||
// @summary Activate OpenAMT device and associate to agent endpoint
|
||||
// @description Activate OpenAMT device and associate to agent endpoint
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @router /open_amt/{id}/activate [post]
|
||||
func (handler *Handler) openAMTActivate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
|
||||
} else if !endpointutils.IsAgentEndpoint(endpoint) {
|
||||
errMsg := fmt.Sprintf("%s is not an agent environment", endpoint.Name)
|
||||
return &httperror.HandlerError{http.StatusBadRequest, errMsg, errors.New(errMsg)}
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
err = handler.activateDevice(endpoint, *settings)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to activate device", err}
|
||||
}
|
||||
|
||||
hostInfo, _, err := handler.getEndpointAMTInfo(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve AMT information", Err: err}
|
||||
}
|
||||
if hostInfo.ControlModeRaw < 1 {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to activate device", Err: errors.New("failed to activate device")}
|
||||
}
|
||||
if hostInfo.UUID == "" {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve device UUID", Err: errors.New("unable to retrieve device UUID")}
|
||||
}
|
||||
|
||||
endpoint.AMTDeviceGUID = hostInfo.UUID
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
@@ -24,11 +24,6 @@ type openAMTConfigureDefaultPayload struct {
|
||||
CertFileText string
|
||||
CertPassword string
|
||||
DomainName string
|
||||
UseWirelessConfig bool
|
||||
WifiAuthenticationMethod string
|
||||
WifiEncryptionMethod string
|
||||
WifiSSID string
|
||||
WifiPskPass string
|
||||
}
|
||||
|
||||
func (payload *openAMTConfigureDefaultPayload) Validate(r *http.Request) error {
|
||||
@@ -51,20 +46,6 @@ func (payload *openAMTConfigureDefaultPayload) Validate(r *http.Request) error {
|
||||
if payload.CertPassword == "" {
|
||||
return errors.New("certificate password must be provided")
|
||||
}
|
||||
if payload.UseWirelessConfig {
|
||||
if payload.WifiAuthenticationMethod == "" {
|
||||
return errors.New("wireless authentication method must be provided")
|
||||
}
|
||||
if payload.WifiEncryptionMethod == "" {
|
||||
return errors.New("wireless encryption method must be provided")
|
||||
}
|
||||
if payload.WifiSSID == "" {
|
||||
return errors.New("wireless config SSID must be provided")
|
||||
}
|
||||
if payload.WifiPskPass == "" {
|
||||
return errors.New("wireless config PSK passphrase must be provided")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -158,15 +139,6 @@ func (handler *Handler) enableOpenAMT(configurationPayload openAMTConfigureDefau
|
||||
},
|
||||
}
|
||||
|
||||
if configurationPayload.UseWirelessConfig {
|
||||
configuration.WirelessConfiguration = &portainer.WirelessConfiguration{
|
||||
AuthenticationMethod: configurationPayload.WifiAuthenticationMethod,
|
||||
EncryptionMethod: configurationPayload.WifiEncryptionMethod,
|
||||
SSID: configurationPayload.WifiSSID,
|
||||
PskPass: configurationPayload.WifiPskPass,
|
||||
}
|
||||
}
|
||||
|
||||
err := handler.OpenAMTService.ConfigureDefault(configuration)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("error configuring OpenAMT server")
|
||||
@@ -215,4 +187,4 @@ func (handler *Handler) disableOpenAMT() error {
|
||||
|
||||
logrus.Info("OpenAMT successfully disabled")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
178
api/http/handler/hostmanagement/openamt/amtdevices.go
Normal file
178
api/http/handler/hostmanagement/openamt/amtdevices.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package openamt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Devices struct {
|
||||
Devices []portainer.OpenAMTDeviceInformation
|
||||
}
|
||||
|
||||
// @id OpenAMTDevices
|
||||
// @summary Fetch OpenAMT managed devices information for endpoint
|
||||
// @description Fetch OpenAMT managed devices information for endpoint
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @router /open_amt/{id}/devices [get]
|
||||
func (handler *Handler) openAMTDevices(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
device, err := handler.OpenAMTService.DeviceInformation(settings.OpenAMTConfiguration, endpoint.AMTDeviceGUID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve device information", err}
|
||||
}
|
||||
|
||||
devicesInformation := Devices{
|
||||
Devices: []portainer.OpenAMTDeviceInformation{
|
||||
*device,
|
||||
},
|
||||
}
|
||||
|
||||
return response.JSON(w, devicesInformation)
|
||||
}
|
||||
|
||||
type deviceActionPayload struct {
|
||||
DeviceID string
|
||||
DeviceAction string
|
||||
}
|
||||
|
||||
func (payload *deviceActionPayload) Validate(r *http.Request) error {
|
||||
if payload.DeviceAction == "" {
|
||||
return errors.New("device action must be provided")
|
||||
}
|
||||
if payload.DeviceID == "" {
|
||||
return errors.New("device GUID must be provided")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id DeviceAction
|
||||
// @summary Execute out of band action on an AMT managed device
|
||||
// @description Execute out of band action on an AMT managed device
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param body body deviceActionPayload true "Device Action"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @router /open_amt/{id}/devices/{deviceId}/{deviceAction} [post]
|
||||
func (handler *Handler) deviceAction(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload deviceActionPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Invalid request payload")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
err = handler.OpenAMTService.ExecuteDeviceAction(settings.OpenAMTConfiguration, payload.DeviceID, payload.DeviceAction)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Error executing device action")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Error executing device action", Err: err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
type deviceFeaturesPayload struct {
|
||||
DeviceID string
|
||||
EnabledFeatures portainer.OpenAMTDeviceEnabledFeatures
|
||||
}
|
||||
|
||||
func (payload *deviceFeaturesPayload) Validate(r *http.Request) error {
|
||||
if payload.DeviceID == "" {
|
||||
return errors.New("device GUID must be provided")
|
||||
}
|
||||
if payload.EnabledFeatures.UserConsent == "" {
|
||||
return errors.New("device user consent status must be provided")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AuthorizationResponse struct {
|
||||
Server string
|
||||
Token string
|
||||
}
|
||||
|
||||
// @id DeviceFeatures
|
||||
// @summary Enable features on an AMT managed device
|
||||
// @description Enable features on an AMT managed device
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param body body deviceFeaturesPayload true "Device Features"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @router /open_amt/{id}/devices_features/{deviceId} [post]
|
||||
func (handler *Handler) deviceFeatures(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload deviceFeaturesPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Invalid request payload")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
_, err = handler.OpenAMTService.DeviceInformation(settings.OpenAMTConfiguration, payload.DeviceID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve device information", err}
|
||||
}
|
||||
|
||||
token, err := handler.OpenAMTService.EnableDeviceFeatures(settings.OpenAMTConfiguration, payload.DeviceID, payload.EnabledFeatures)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Error executing device action")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Error executing device action", Err: err}
|
||||
}
|
||||
|
||||
credentials := AuthorizationResponse{
|
||||
Server: settings.OpenAMTConfiguration.MPSServer,
|
||||
Token: token,
|
||||
}
|
||||
return response.JSON(w, credentials)
|
||||
}
|
||||
278
api/http/handler/hostmanagement/openamt/amtrpc.go
Normal file
278
api/http/handler/hostmanagement/openamt/amtrpc.go
Normal file
@@ -0,0 +1,278 @@
|
||||
package openamt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/portainer/portainer/api/hostmanagement/openamt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type HostInfo struct {
|
||||
EndpointID portainer.EndpointID `json:"EndpointID"`
|
||||
RawOutput string `json:"RawOutput"`
|
||||
AMT string `json:"AMT"`
|
||||
UUID string `json:"UUID"`
|
||||
DNSSuffix string `json:"DNS Suffix"`
|
||||
BuildNumber string `json:"Build Number"`
|
||||
ControlMode string `json:"Control Mode"`
|
||||
ControlModeRaw int `json:"Control Mode (Raw)"`
|
||||
}
|
||||
|
||||
const (
|
||||
// TODO: this should get extracted to some configurable - don't assume Docker Hub is everyone's global namespace, or that they're allowed to pull images from the internet
|
||||
rpcGoImageName = "intel/oact-rpc-go"
|
||||
rpcGoContainerName = "oact-rpc-go"
|
||||
dockerClientTimeout = 5 * time.Minute
|
||||
)
|
||||
|
||||
// @id OpenAMTHostInfo
|
||||
// @summary Request OpenAMT info from a node
|
||||
// @description Request OpenAMT info from a node
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @router /open_amt/{id}/info [get]
|
||||
func (handler *Handler) openAMTHostInfo(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
logrus.WithField("endpointID", endpointID).Info("OpenAMTHostInfo")
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
|
||||
}
|
||||
|
||||
amtInfo, output, err := handler.getEndpointAMTInfo(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: output, Err: err}
|
||||
}
|
||||
|
||||
return response.JSON(w, amtInfo)
|
||||
}
|
||||
|
||||
func (handler *Handler) getEndpointAMTInfo(endpoint *portainer.Endpoint) (*HostInfo, string, error) {
|
||||
ctx := context.TODO()
|
||||
|
||||
// pull the image so we can check if there's a new one
|
||||
// TODO: these should be able to be over-ridden (don't hardcode the assumption that secure users can access Docker Hub, or that its even the orchestrator's "global namespace")
|
||||
cmdLine := []string{"amtinfo", "--json"}
|
||||
output, err := handler.PullAndRunContainer(ctx, endpoint, rpcGoImageName, rpcGoContainerName, cmdLine)
|
||||
if err != nil {
|
||||
return nil, output, err
|
||||
}
|
||||
|
||||
amtInfo := HostInfo{}
|
||||
_ = json.Unmarshal([]byte(output), &amtInfo)
|
||||
|
||||
amtInfo.EndpointID = endpoint.ID
|
||||
amtInfo.RawOutput = output
|
||||
|
||||
return &amtInfo, "", nil
|
||||
}
|
||||
|
||||
func (handler *Handler) PullAndRunContainer(ctx context.Context, endpoint *portainer.Endpoint, imageName, containerName string, cmdLine []string) (output string, err error) {
|
||||
// TODO: this should not be Docker specific
|
||||
// TODO: extract from this Handler into something global.
|
||||
|
||||
// TODO: start
|
||||
// docker run --rm -it --privileged intel/oact-rpc-go amtinfo
|
||||
// on the Docker standalone node (one per env :)
|
||||
// and later, on the specified node in the swarm, or kube.
|
||||
nodeName := ""
|
||||
timeout := dockerClientTimeout
|
||||
docker, err := handler.DockerClientFactory.CreateClient(endpoint, nodeName, &timeout)
|
||||
if err != nil {
|
||||
return "Unable to create Docker Client connection", err
|
||||
}
|
||||
defer docker.Close()
|
||||
|
||||
if err := pullImage(ctx, docker, imageName); err != nil {
|
||||
return "Could not pull image from registry", err
|
||||
}
|
||||
|
||||
output, err = runContainer(ctx, docker, imageName, containerName, cmdLine)
|
||||
if err != nil {
|
||||
return "Could not run container", err
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// TODO: ideally, pullImage and runContainer will become a simple version of the use compose abstraction that can be called from withing Portainer.
|
||||
// TODO: the idea being that if we have an internal struct of a parsed compose file, we can also populate that struct programmatically, and run it to get the result I'm getting here.
|
||||
// TODO: likely an upgrade and abstraction of DeployComposeStack/DeploySwarmStack/DeployKubernetesStack
|
||||
// pullImage will pull the image to the specified environment
|
||||
// TODO: add k8s implementation
|
||||
// TODO: work out registry auth
|
||||
func pullImage(ctx context.Context, docker *client.Client, imageName string) error {
|
||||
out, err := docker.ImagePull(ctx, imageName, types.ImagePullOptions{})
|
||||
if err != nil {
|
||||
logrus.WithError(err).WithField("imageName", imageName).Error("Could not pull image from registry")
|
||||
return err
|
||||
}
|
||||
|
||||
defer out.Close()
|
||||
outputBytes, err := ioutil.ReadAll(out)
|
||||
if err != nil {
|
||||
logrus.WithError(err).WithField("imageName", imageName).Error("Could not read image pull output")
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("%s imaged pulled with output:\n%s", imageName, string(outputBytes))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: ideally, pullImage and runContainer will become a simple version of the use compose abstraction that can be called from withing Portainer.
|
||||
// runContainer should be used to run a short command that returns information to stdout
|
||||
// TODO: add k8s support
|
||||
func runContainer(ctx context.Context, docker *client.Client, imageName, containerName string, cmdLine []string) (output string, err error) {
|
||||
opts := types.ContainerListOptions{All: true}
|
||||
opts.Filters = filters.NewArgs()
|
||||
opts.Filters.Add("name", containerName)
|
||||
existingContainers, err := docker.ContainerList(ctx, opts)
|
||||
if err != nil {
|
||||
logrus.WithError(err).WithField("imagename", imageName).WithField("containername", containerName).Error("listing existing container")
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(existingContainers) > 0 {
|
||||
err = docker.ContainerRemove(ctx, existingContainers[0].ID, types.ContainerRemoveOptions{Force: true})
|
||||
if err != nil {
|
||||
logrus.WithError(err).WithField("imagename", imageName).WithField("containername", containerName).Error("removing existing container")
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
created, err := docker.ContainerCreate(
|
||||
ctx,
|
||||
&container.Config{
|
||||
Image: imageName,
|
||||
Cmd: cmdLine,
|
||||
Env: []string{},
|
||||
Tty: true,
|
||||
OpenStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
},
|
||||
&container.HostConfig{
|
||||
Privileged: true,
|
||||
},
|
||||
&network.NetworkingConfig{},
|
||||
nil,
|
||||
containerName,
|
||||
)
|
||||
if err != nil {
|
||||
logrus.WithError(err).WithField("imagename", imageName).WithField("containername", containerName).Error("creating container")
|
||||
return "", err
|
||||
}
|
||||
err = docker.ContainerStart(ctx, created.ID, types.ContainerStartOptions{})
|
||||
if err != nil {
|
||||
logrus.WithError(err).WithField("imagename", imageName).WithField("containername", containerName).Error("starting container")
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Printf("%s container created and started\n", containerName)
|
||||
|
||||
statusCh, errCh := docker.ContainerWait(ctx, created.ID, container.WaitConditionNotRunning)
|
||||
var statusCode int64
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
logrus.WithError(err).WithField("imagename", imageName).WithField("containername", containerName).Error("starting container")
|
||||
return "", err
|
||||
}
|
||||
case status := <-statusCh:
|
||||
statusCode = status.StatusCode
|
||||
}
|
||||
logrus.WithField("status", statusCode).Debug("container wait status")
|
||||
|
||||
out, err := docker.ContainerLogs(ctx, created.ID, types.ContainerLogsOptions{ShowStdout: true})
|
||||
if err != nil {
|
||||
logrus.WithError(err).WithField("imagename", imageName).WithField("containername", containerName).Error("getting container log")
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = docker.ContainerRemove(ctx, created.ID, types.ContainerRemoveOptions{})
|
||||
if err != nil {
|
||||
logrus.WithError(err).WithField("imagename", imageName).WithField("containername", containerName).Error("removing container")
|
||||
return "", err
|
||||
}
|
||||
|
||||
outputBytes, err := ioutil.ReadAll(out)
|
||||
if err != nil {
|
||||
logrus.WithError(err).WithField("imagename", imageName).WithField("containername", containerName).Error("read container output")
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Printf("%s container finished with output:\n%s", containerName, string(outputBytes))
|
||||
|
||||
return string(outputBytes), nil
|
||||
}
|
||||
|
||||
func (handler *Handler) activateDevice(endpoint *portainer.Endpoint, settings portainer.Settings) error {
|
||||
ctx := context.TODO()
|
||||
|
||||
config := settings.OpenAMTConfiguration
|
||||
cmdLine := []string{
|
||||
"activate",
|
||||
"-n",
|
||||
"-v",
|
||||
"-u", fmt.Sprintf("wss://%s/activate", config.MPSServer),
|
||||
"-profile", openamt.DefaultProfileName,
|
||||
"-d", config.DomainConfiguration.DomainName,
|
||||
"-password", config.Credentials.MPSPassword,
|
||||
}
|
||||
|
||||
_, err := handler.PullAndRunContainer(ctx, endpoint, rpcGoImageName, rpcGoContainerName, cmdLine)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) deactivateDevice(endpoint *portainer.Endpoint, settings portainer.Settings) error {
|
||||
ctx := context.TODO()
|
||||
|
||||
config := settings.OpenAMTConfiguration
|
||||
cmdLine := []string{
|
||||
"deactivate",
|
||||
"-n",
|
||||
"-v",
|
||||
"-u", fmt.Sprintf("wss://%s/activate", config.MPSServer),
|
||||
"-password", config.Credentials.MPSPassword,
|
||||
}
|
||||
_, err := handler.PullAndRunContainer(ctx, endpoint, rpcGoImageName, rpcGoContainerName, cmdLine)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
@@ -16,6 +17,7 @@ type Handler struct {
|
||||
*mux.Router
|
||||
OpenAMTService portainer.OpenAMTService
|
||||
DataStore dataservices.DataStore
|
||||
DockerClientFactory *docker.ClientFactory
|
||||
}
|
||||
|
||||
// NewHandler returns a new Handler
|
||||
@@ -29,6 +31,11 @@ func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataSto
|
||||
}
|
||||
|
||||
h.Handle("/open_amt", bouncer.AdminAccess(httperror.LoggerHandler(h.openAMTConfigureDefault))).Methods(http.MethodPost)
|
||||
h.Handle("/open_amt/{id}/info", bouncer.AdminAccess(httperror.LoggerHandler(h.openAMTHostInfo))).Methods(http.MethodGet)
|
||||
h.Handle("/open_amt/{id}/activate", bouncer.AdminAccess(httperror.LoggerHandler(h.openAMTActivate))).Methods(http.MethodPost)
|
||||
h.Handle("/open_amt/{id}/devices", bouncer.AdminAccess(httperror.LoggerHandler(h.openAMTDevices))).Methods(http.MethodGet)
|
||||
h.Handle("/open_amt/{id}/devices/{deviceId}/{deviceAction}", bouncer.AdminAccess(httperror.LoggerHandler(h.deviceAction))).Methods(http.MethodPost)
|
||||
h.Handle("/open_amt/{id}/devices_features/{deviceId}", bouncer.AdminAccess(httperror.LoggerHandler(h.deviceFeatures))).Methods(http.MethodPost)
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ func (handler *Handler) checkUniqueStackNameInDocker(endpoint *portainer.Endpoin
|
||||
return false, err
|
||||
}
|
||||
|
||||
dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint, "")
|
||||
dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint, "", nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ func (handler *Handler) executeServiceWebhook(
|
||||
registryID portainer.RegistryID,
|
||||
imageTag string,
|
||||
) *httperror.HandlerError {
|
||||
dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint, "")
|
||||
dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint, "", nil)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Error creating docker client", err}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ func (transport *Transport) createPrivateResourceControl(resourceIdentifier stri
|
||||
}
|
||||
|
||||
func (transport *Transport) getInheritedResourceControlFromServiceOrStack(resourceIdentifier, nodeName string, resourceType portainer.ResourceControlType, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
client, err := transport.dockerClientFactory.CreateClient(transport.endpoint, nodeName)
|
||||
client, err := transport.dockerClientFactory.CreateClient(transport.endpoint, nodeName, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ func (transport *Transport) decorateVolumeResourceCreationOperation(request *htt
|
||||
|
||||
if volumeID != "" {
|
||||
agentTargetHeader := request.Header.Get(portainer.PortainerAgentTargetHeader)
|
||||
cli, err := transport.dockerClientFactory.CreateClient(transport.endpoint, agentTargetHeader)
|
||||
cli, err := transport.dockerClientFactory.CreateClient(transport.endpoint, agentTargetHeader, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -219,7 +219,7 @@ func (transport *Transport) getDockerID() (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
client, err := transport.dockerClientFactory.CreateClient(transport.endpoint, "")
|
||||
client, err := transport.dockerClientFactory.CreateClient(transport.endpoint, "", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/portainer/portainer/api/http/handler/endpoints"
|
||||
"github.com/portainer/portainer/api/http/handler/file"
|
||||
"github.com/portainer/portainer/api/http/handler/helm"
|
||||
"github.com/portainer/portainer/api/http/handler/hostmanagement/fdo"
|
||||
"github.com/portainer/portainer/api/http/handler/hostmanagement/openamt"
|
||||
kubehandler "github.com/portainer/portainer/api/http/handler/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/handler/ldap"
|
||||
@@ -216,8 +217,11 @@ func (server *Server) Start() error {
|
||||
if openAMTHandler != nil {
|
||||
openAMTHandler.OpenAMTService = server.OpenAMTService
|
||||
openAMTHandler.DataStore = server.DataStore
|
||||
openAMTHandler.DockerClientFactory = server.DockerClientFactory
|
||||
}
|
||||
|
||||
fdoHandler := fdo.NewHandler(requestBouncer, server.DataStore)
|
||||
|
||||
var stackHandler = stacks.NewHandler(requestBouncer)
|
||||
stackHandler.DataStore = server.DataStore
|
||||
stackHandler.DockerClientFactory = server.DockerClientFactory
|
||||
@@ -284,6 +288,7 @@ func (server *Server) Start() error {
|
||||
KubernetesHandler: kubernetesHandler,
|
||||
MOTDHandler: motdHandler,
|
||||
OpenAMTHandler: openAMTHandler,
|
||||
FDOHandler: fdoHandler,
|
||||
RegistryHandler: registryHandler,
|
||||
ResourceControlHandler: resourceControlHandler,
|
||||
SettingsHandler: settingsHandler,
|
||||
|
||||
@@ -46,6 +46,23 @@ func Test_IsKubernetesEndpoint(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_IsAgentEndpoint(t *testing.T) {
|
||||
tests := []isEndpointTypeTest{
|
||||
{endpointType: portainer.DockerEnvironment, expected: false},
|
||||
{endpointType: portainer.AgentOnDockerEnvironment, expected: true},
|
||||
{endpointType: portainer.AzureEnvironment, expected: false},
|
||||
{endpointType: portainer.EdgeAgentOnDockerEnvironment, expected: true},
|
||||
{endpointType: portainer.KubernetesLocalEnvironment, expected: false},
|
||||
{endpointType: portainer.AgentOnKubernetesEnvironment, expected: true},
|
||||
{endpointType: portainer.EdgeAgentOnKubernetesEnvironment, expected: true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
ans := IsAgentEndpoint(&portainer.Endpoint{Type: test.endpointType})
|
||||
assert.Equal(t, test.expected, ans)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_FilterByExcludeIDs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -101,4 +118,4 @@ func Test_FilterByExcludeIDs(t *testing.T) {
|
||||
tt.asserts(t, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,14 @@ func IsEdgeEndpoint(endpoint *portainer.Endpoint) bool {
|
||||
return endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment
|
||||
}
|
||||
|
||||
// IsAgentEndpoint returns true if this is an Agent endpoint
|
||||
func IsAgentEndpoint(endpoint *portainer.Endpoint) bool {
|
||||
return endpoint.Type == portainer.AgentOnDockerEnvironment ||
|
||||
endpoint.Type == portainer.EdgeAgentOnDockerEnvironment ||
|
||||
endpoint.Type == portainer.AgentOnKubernetesEnvironment ||
|
||||
endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment
|
||||
}
|
||||
|
||||
// FilterByExcludeIDs receives an environment(endpoint) array and returns a filtered array using an excludeIds param
|
||||
func FilterByExcludeIDs(endpoints []portainer.Endpoint, excludeIds []portainer.EndpointID) []portainer.Endpoint {
|
||||
if len(excludeIds) == 0 {
|
||||
|
||||
@@ -45,7 +45,6 @@ type (
|
||||
MPSServer string `json:"MPSServer"`
|
||||
Credentials MPSCredentials `json:"Credentials"`
|
||||
DomainConfiguration DomainConfiguration `json:"DomainConfiguration"`
|
||||
WirelessConfiguration *WirelessConfiguration `json:"WirelessConfiguration"`
|
||||
}
|
||||
|
||||
MPSCredentials struct {
|
||||
@@ -60,11 +59,32 @@ type (
|
||||
DomainName string `json:"DomainName"`
|
||||
}
|
||||
|
||||
WirelessConfiguration struct {
|
||||
AuthenticationMethod string `json:"AuthenticationMethod"`
|
||||
EncryptionMethod string `json:"EncryptionMethod"`
|
||||
SSID string `json:"SSID"`
|
||||
PskPass string `json:"PskPass"`
|
||||
// OpenAMTDeviceInformation represents an AMT managed device information
|
||||
OpenAMTDeviceInformation struct {
|
||||
GUID string `json:"guid"`
|
||||
HostName string `json:"hostname"`
|
||||
ConnectionStatus bool `json:"connectionStatus"`
|
||||
PowerState PowerState `json:"powerstate"`
|
||||
EnabledFeatures *OpenAMTDeviceEnabledFeatures `json:"features"`
|
||||
}
|
||||
|
||||
// OpenAMTDeviceEnabledFeatures represents an AMT managed device features information
|
||||
OpenAMTDeviceEnabledFeatures struct {
|
||||
Redirection bool `json:"redirection"`
|
||||
KVM bool `json:"KVM"`
|
||||
SOL bool `json:"SOL"`
|
||||
IDER bool `json:"IDER"`
|
||||
UserConsent string `json:"userConsent"`
|
||||
}
|
||||
|
||||
// PowerState represents an AMT managed device power state
|
||||
PowerState int
|
||||
|
||||
FDOConfiguration struct {
|
||||
Enabled bool `json:"Enabled"`
|
||||
OwnerURL string `json:"OwnerURL"`
|
||||
OwnerUsername string `json:"OwnerUsername"`
|
||||
OwnerPassword string `json:"OwnerPassword"`
|
||||
}
|
||||
|
||||
// CLIFlags represents the available flags on the CLI
|
||||
@@ -294,6 +314,8 @@ type (
|
||||
ComposeSyntaxMaxVersion string `json:"ComposeSyntaxMaxVersion" example:"3.8"`
|
||||
// Environment(Endpoint) specific security settings
|
||||
SecuritySettings EndpointSecuritySettings
|
||||
// The identifier of the AMT Device associated with this environment(endpoint)
|
||||
AMTDeviceGUID string `json:"AMTDeviceGUID,omitempty" example:"4c4c4544-004b-3910-8037-b6c04f504633"`
|
||||
// LastCheckInDate mark last check-in date on checkin
|
||||
LastCheckInDate int64
|
||||
|
||||
@@ -765,6 +787,7 @@ type (
|
||||
LDAPSettings LDAPSettings `json:"LDAPSettings" example:""`
|
||||
OAuthSettings OAuthSettings `json:"OAuthSettings" example:""`
|
||||
OpenAMTConfiguration OpenAMTConfiguration `json:"OpenAMTConfiguration" example:""`
|
||||
FDOConfiguration FDOConfiguration `json:"FDOConfiguration" example:""`
|
||||
FeatureFlagSettings map[Feature]bool `json:"FeatureFlagSettings" example:""`
|
||||
// The interval in which environment(endpoint) snapshots are created
|
||||
SnapshotInterval string `json:"SnapshotInterval" example:"5m"`
|
||||
@@ -1218,7 +1241,11 @@ type (
|
||||
|
||||
// OpenAMTService represents a service for managing OpenAMT
|
||||
OpenAMTService interface {
|
||||
Authorization(configuration OpenAMTConfiguration) (string, error)
|
||||
ConfigureDefault(configuration OpenAMTConfiguration) error
|
||||
DeviceInformation(configuration OpenAMTConfiguration, deviceGUID string) (*OpenAMTDeviceInformation, error)
|
||||
EnableDeviceFeatures(configuration OpenAMTConfiguration, deviceGUID string, features OpenAMTDeviceEnabledFeatures) (string, error)
|
||||
ExecuteDeviceAction(configuration OpenAMTConfiguration, deviceGUID string, action string) error
|
||||
}
|
||||
|
||||
// KubeClient represents a service used to query a Kubernetes environment(endpoint)
|
||||
|
||||
@@ -885,3 +885,8 @@ json-tree .branch-preview {
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.icon-disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
2
app/global.d.ts
vendored
2
app/global.d.ts
vendored
@@ -6,3 +6,5 @@ declare module '*.png' {
|
||||
}
|
||||
|
||||
declare module '*.css';
|
||||
|
||||
declare module '@open-amt-cloud-toolkit/ui-toolkit-react/reactjs/src/kvm.bundle';
|
||||
|
||||
@@ -203,6 +203,17 @@ angular
|
||||
},
|
||||
};
|
||||
|
||||
var deviceImport = {
|
||||
name: 'portainer.endpoints.importdevice',
|
||||
url: '/device',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/devices/import/importDevice.html',
|
||||
controller: 'ImportDeviceController',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var endpointAccess = {
|
||||
name: 'portainer.endpoints.endpoint.access',
|
||||
url: '/access',
|
||||
@@ -215,6 +226,17 @@ angular
|
||||
},
|
||||
};
|
||||
|
||||
var endpointKVM = {
|
||||
name: 'portainer.endpoints.endpoint.kvm',
|
||||
url: '/kvm?deviceId&deviceName',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/endpoints/kvm/endpointKVM.html',
|
||||
controller: 'EndpointKVMController',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var groups = {
|
||||
name: 'portainer.groups',
|
||||
url: '/groups',
|
||||
@@ -442,7 +464,9 @@ angular
|
||||
$stateRegistryProvider.register(endpoint);
|
||||
$stateRegistryProvider.register(k8sendpoint);
|
||||
$stateRegistryProvider.register(endpointAccess);
|
||||
$stateRegistryProvider.register(endpointKVM);
|
||||
$stateRegistryProvider.register(endpointCreation);
|
||||
$stateRegistryProvider.register(deviceImport);
|
||||
$stateRegistryProvider.register(endpointKubernetesConfiguration);
|
||||
$stateRegistryProvider.register(groups);
|
||||
$stateRegistryProvider.register(group);
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<div class="amt-devices-datatable">
|
||||
<table class="table table-condensed table-hover nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>MPS Status</th>
|
||||
<th>Power State</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="device in $ctrl.devices">
|
||||
<td>{{ device.hostname }}</td>
|
||||
<td>{{ device.connectionStatus ? 'Connected' : 'Disconnected' }}</td>
|
||||
<td>{{ $ctrl.parsePowerState(device.powerstate) }} <i class="fa fa-cog fa-spin" ng-show="$ctrl.state.executingAction[device.guid]" style="margin-left: 5px;"></i></td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-xs" role="group" aria-label="..." style="display: inline-flex;">
|
||||
<a style="margin: 0 2.5px;" title="Power up the device" ng-click="$ctrl.executeDeviceAction(device, 'power up')">
|
||||
<i class="fa fa-plug space-right" ng-class="{ 'icon-disabled': !device.connectionStatus }" aria-hidden="true"></i>
|
||||
</a>
|
||||
<a style="margin: 0 2.5px;" title="Power off the device" ng-click="$ctrl.executeDeviceAction(device, 'power off')">
|
||||
<i class="fa fa-power-off space-right" ng-class="{ 'icon-disabled': !device.connectionStatus }" aria-hidden="true"></i>
|
||||
</a>
|
||||
<a style="margin: 0 2.5px;" title="Restart the device" ng-click="$ctrl.executeDeviceAction(device, 'restart')">
|
||||
<i class="fa fa-sync space-right" ng-class="{ 'icon-disabled': !device.connectionStatus }" aria-hidden="true"></i>
|
||||
</a>
|
||||
<a style="margin: 0 2.5px;" title="Connect KVM" target="_blank" ui-sref="portainer.endpoints.endpoint.kvm({id: $ctrl.endpointId, deviceId: device.guid, deviceName: device.hostname})">
|
||||
<i class="fa fa-desktop space-right" ng-class="{ 'icon-disabled': !device.connectionStatus }" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.devices && !$ctrl.error">
|
||||
<td colspan="5" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.devices.length === 0">
|
||||
<td colspan="5" class="text-center text-muted">No devices found.</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.error">
|
||||
<td colspan="5" class="text-center text-muted">{{ $ctrl.error }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
angular.module('portainer.docker').component('amtDevicesDatatable', {
|
||||
templateUrl: './amtDevicesDatatable.html',
|
||||
controller: 'AMTDevicesDatatableController',
|
||||
bindings: {
|
||||
endpointId: '<',
|
||||
devices: '<',
|
||||
error: '<',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
angular.module('portainer.docker').controller('AMTDevicesDatatableController', [
|
||||
'$scope',
|
||||
'$state',
|
||||
'$controller',
|
||||
'OpenAMTService',
|
||||
'ModalService',
|
||||
'Notifications',
|
||||
function ($scope, $state, $controller, OpenAMTService, ModalService, Notifications) {
|
||||
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope, $state: $state }));
|
||||
|
||||
this.state = Object.assign(this.state, {
|
||||
executingAction: {},
|
||||
});
|
||||
|
||||
this.parsePowerState = function (powerStateIntValue) {
|
||||
// https://app.swaggerhub.com/apis-docs/rbheopenamt/mps/1.4.0#/AMT/get_api_v1_amt_power_state__guid_
|
||||
switch (powerStateIntValue) {
|
||||
case 2:
|
||||
return 'Running';
|
||||
case 3:
|
||||
case 4:
|
||||
return 'Sleep';
|
||||
case 6:
|
||||
case 8:
|
||||
case 13:
|
||||
return 'Off';
|
||||
case 7:
|
||||
return 'Hibernate';
|
||||
case 9:
|
||||
return 'Power Cycle';
|
||||
default:
|
||||
return '-';
|
||||
}
|
||||
};
|
||||
|
||||
this.executeDeviceAction = async function (device, action) {
|
||||
const deviceGUID = device.guid;
|
||||
if (!device.connectionStatus) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const confirmed = await ModalService.confirmAsync({
|
||||
title: `Confirm action`,
|
||||
message: `Are you sure you want to ${action} the device?`,
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Confirm',
|
||||
className: 'btn-warning',
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
this.state.executingAction[deviceGUID] = true;
|
||||
|
||||
await OpenAMTService.executeDeviceAction(this.endpointId, deviceGUID, action);
|
||||
Notifications.success(`${action} action sent successfully`);
|
||||
$state.reload();
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
Notifications.error('Failure', err, `Failed to ${action} the device`);
|
||||
} finally {
|
||||
this.state.executingAction[deviceGUID] = false;
|
||||
}
|
||||
};
|
||||
|
||||
this.$onInit = function () {
|
||||
this.setDefaults();
|
||||
};
|
||||
},
|
||||
]);
|
||||
@@ -15,7 +15,20 @@
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.endpoints.new" data-cy="endpoint-addEndpointButton">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add environment
|
||||
<i class="fa fa-plus-circle space-right" aria-hidden="true"></i>Add environment
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
ng-show="$ctrl.state.showAMTInfo"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0"
|
||||
ng-click="$ctrl.associateOpenAMT($ctrl.state.selectedItems)"
|
||||
data-cy="endpoint-associateOpenAMTButton"
|
||||
>
|
||||
<i class="fa fa-link space-right" aria-hidden="true"></i>Associate with OpenAMT
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ng-show="$ctrl.state.showFDOInfo" ui-sref="portainer.endpoints.importdevice" data-cy="endpoint-importFDODeviceButton">
|
||||
<i class="fa fa-plus-circle space-right" aria-hidden="true"></i>Import Device
|
||||
</button>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
@@ -40,6 +53,8 @@
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
Name
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
|
||||
@@ -72,7 +87,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
dir-paginate="item in $ctrl.state.filteredDataSet | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit"
|
||||
dir-paginate-start="item in $ctrl.state.filteredDataSet | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit"
|
||||
total-items="$ctrl.state.totalFilteredDataSet"
|
||||
ng-class="{ active: item.Checked }"
|
||||
>
|
||||
@@ -81,7 +96,12 @@
|
||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" />
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a ui-sref="portainer.endpoints.endpoint({id: item.Id})">{{ item.Name }}</a>
|
||||
<a style="float: right;" ng-show="$ctrl.showAMTNodes(item)" ng-click="$ctrl.expandItem(item, !item.Expanded)">
|
||||
<i ng-class="{ 'fas fa-angle-down': item.Expanded, 'fas fa-angle-right': !item.Expanded }" class="space-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
@@ -98,6 +118,12 @@
|
||||
<a ui-sref="portainer.endpoints.endpoint.access({id: item.Id})"> <i class="fa fa-users" aria-hidden="true"></i> Manage access </a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr dir-paginate-end ng-show="item.Expanded">
|
||||
<td></td>
|
||||
<td colspan="8">
|
||||
<amt-devices-datatable endpoint-id="item.Id" devices="$ctrl.state.amtDevices[item.Id]" error="$ctrl.state.amtDevicesErrors[item.Id]"> </amt-devices-datatable>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.loading">
|
||||
<td colspan="5" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
|
||||
@@ -9,5 +9,6 @@ angular.module('portainer.app').component('endpointsDatatable', {
|
||||
reverseOrder: '<',
|
||||
removeAction: '<',
|
||||
retrievePage: '<',
|
||||
setLoadingMessage: '<',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import EndpointHelper from '@/portainer/helpers/endpointHelper';
|
||||
|
||||
angular.module('portainer.app').controller('EndpointsDatatableController', [
|
||||
'$scope',
|
||||
'$state',
|
||||
'$controller',
|
||||
'DatatableService',
|
||||
'PaginationService',
|
||||
function ($scope, $controller, DatatableService, PaginationService) {
|
||||
'SettingsService',
|
||||
'ModalService',
|
||||
'Notifications',
|
||||
'OpenAMTService',
|
||||
function ($scope, $state, $controller, DatatableService, PaginationService, SettingsService, ModalService, Notifications, OpenAMTService) {
|
||||
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
||||
|
||||
this.state = Object.assign(this.state, {
|
||||
@@ -12,6 +19,10 @@ angular.module('portainer.app').controller('EndpointsDatatableController', [
|
||||
filteredDataSet: [],
|
||||
totalFilteredDataset: 0,
|
||||
pageNumber: 1,
|
||||
showAMTInfo: false,
|
||||
amtDevices: {},
|
||||
amtDevicesErrors: {},
|
||||
showFDOInfo: false,
|
||||
});
|
||||
|
||||
this.paginationChanged = function () {
|
||||
@@ -50,10 +61,81 @@ angular.module('portainer.app').controller('EndpointsDatatableController', [
|
||||
this.paginationChanged();
|
||||
};
|
||||
|
||||
this.setShowIntelInfo = async function () {
|
||||
this.settings = await SettingsService.settings();
|
||||
const openAMTFeatureFlagValue = this.settings && this.settings.FeatureFlagSettings && this.settings.FeatureFlagSettings['open-amt'];
|
||||
const openAMTFeatureEnabled = this.settings && this.settings.OpenAMTConfiguration && this.settings.OpenAMTConfiguration.Enabled;
|
||||
this.state.showAMTInfo = openAMTFeatureFlagValue && openAMTFeatureEnabled;
|
||||
|
||||
const fdoFeatureFlagValue = this.settings && this.settings.FeatureFlagSettings && this.settings.FeatureFlagSettings['fdo'];
|
||||
const fdoFeatureEnabled = this.settings && this.settings.FDOConfiguration && this.settings.FDOConfiguration.Enabled;
|
||||
this.state.showFDOInfo = fdoFeatureFlagValue && fdoFeatureEnabled;
|
||||
};
|
||||
|
||||
this.showAMTNodes = function (item) {
|
||||
return this.state.showAMTInfo && EndpointHelper.isAgentEndpoint(item) && item.AMTDeviceGUID;
|
||||
};
|
||||
|
||||
this.expandItem = function (item, expanded) {
|
||||
if (!this.showAMTNodes(item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.Expanded = expanded;
|
||||
this.fetchAMTDeviceInfo(item);
|
||||
};
|
||||
|
||||
this.fetchAMTDeviceInfo = function (endpoint) {
|
||||
if (!this.showAMTNodes(endpoint) || this.state.amtDevices[endpoint.Id]) {
|
||||
return;
|
||||
}
|
||||
|
||||
OpenAMTService.getDevices(endpoint.Id)
|
||||
.then((data) => {
|
||||
this.state.amtDevices[endpoint.Id] = data.Devices;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
this.state.amtDevicesErrors[endpoint.Id] = 'Error fetching devices information: ' + e.data.details;
|
||||
});
|
||||
};
|
||||
|
||||
this.associateOpenAMT = function (endpoints) {
|
||||
const setLoadingMessage = this.setLoadingMessage;
|
||||
ModalService.confirm({
|
||||
title: 'Are you sure?',
|
||||
message: 'This operation will associate the selected environments with OpenAMT.',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Associate',
|
||||
className: 'btn-primary',
|
||||
},
|
||||
},
|
||||
callback: async function onConfirm(confirmed) {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingMessage('Activating Active Management Technology on selected devices...');
|
||||
for (let endpoint of endpoints) {
|
||||
try {
|
||||
await OpenAMTService.activateDevice(endpoint.Id);
|
||||
|
||||
Notifications.success('Successfully associated with OpenAMT', endpoint.Name);
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to associate with OpenAMT');
|
||||
}
|
||||
}
|
||||
|
||||
$state.reload();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Overridden
|
||||
*/
|
||||
this.$onInit = function () {
|
||||
this.$onInit = async function () {
|
||||
this.setDefaults();
|
||||
this.prepareTableFromDataset();
|
||||
|
||||
@@ -77,6 +159,7 @@ angular.module('portainer.app').controller('EndpointsDatatableController', [
|
||||
this.filters.state.open = false;
|
||||
}
|
||||
|
||||
await this.setShowIntelInfo();
|
||||
this.paginationChanged();
|
||||
};
|
||||
},
|
||||
|
||||
@@ -12,6 +12,10 @@ export default class EndpointHelper {
|
||||
return endpoint.URL.includes('unix://') || endpoint.URL.includes('npipe://') || endpoint.Type === PortainerEndpointTypes.KubernetesLocalEnvironment;
|
||||
}
|
||||
|
||||
static isDockerEndpoint(endpoint) {
|
||||
return [PortainerEndpointTypes.DockerEnvironment, PortainerEndpointTypes.AgentOnDockerEnvironment, PortainerEndpointTypes.EdgeAgentOnDockerEnvironment].includes(endpoint.Type);
|
||||
}
|
||||
|
||||
static isAgentEndpoint(endpoint) {
|
||||
return [
|
||||
PortainerEndpointTypes.AgentOnDockerEnvironment,
|
||||
|
||||
@@ -5,6 +5,7 @@ export function SettingsViewModel(data) {
|
||||
this.LDAPSettings = data.LDAPSettings;
|
||||
this.OAuthSettings = new OAuthSettingsViewModel(data.OAuthSettings);
|
||||
this.OpenAMTConfiguration = data.OpenAMTConfiguration;
|
||||
this.FDOConfiguration = data.FDOConfiguration;
|
||||
this.SnapshotInterval = data.SnapshotInterval;
|
||||
this.TemplatesURL = data.TemplatesURL;
|
||||
this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval;
|
||||
|
||||
17
app/portainer/rest/fdo.js
Normal file
17
app/portainer/rest/fdo.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import angular from 'angular';
|
||||
|
||||
const API_ENDPOINT_FDO = 'api/fdo';
|
||||
|
||||
angular.module('portainer.app').factory('FDO', FDOFactory);
|
||||
|
||||
/* @ngInject */
|
||||
function FDOFactory($resource) {
|
||||
return $resource(
|
||||
API_ENDPOINT_FDO + '/:action/:deviceId',
|
||||
{},
|
||||
{
|
||||
submit: { method: 'POST' },
|
||||
configureDevice: { method: 'POST', params: { action: 'configure', id: '@deviceId' } },
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -7,10 +7,21 @@ angular.module('portainer.app').factory('OpenAMT', OpenAMTFactory);
|
||||
/* @ngInject */
|
||||
function OpenAMTFactory($resource) {
|
||||
return $resource(
|
||||
API_ENDPOINT_OPEN_AMT,
|
||||
API_ENDPOINT_OPEN_AMT + '/:id/:action/:deviceId/:deviceAction',
|
||||
{},
|
||||
{
|
||||
submit: { method: 'POST' },
|
||||
info: { method: 'GET', params: { id: '@id', action: 'info' } },
|
||||
activate: { method: 'POST', params: { id: '@id', action: 'activate' } },
|
||||
getDevices: { method: 'GET', params: { id: '@id', action: 'devices' } },
|
||||
executeDeviceAction: {
|
||||
method: 'POST',
|
||||
params: { id: '@id', action: 'devices', deviceId: '@deviceId', deviceAction: '@deviceAction' },
|
||||
},
|
||||
enableDeviceFeatures: {
|
||||
method: 'POST',
|
||||
params: { id: '@id', action: 'devices_features', deviceId: '@deviceId' },
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
19
app/portainer/services/api/FDOService.js
Normal file
19
app/portainer/services/api/FDOService.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import angular from 'angular';
|
||||
|
||||
angular.module('portainer.app').service('FDOService', FDOServiceFactory);
|
||||
|
||||
/* @ngInject */
|
||||
function FDOServiceFactory(FDO) {
|
||||
return {
|
||||
submit,
|
||||
configureDevice,
|
||||
};
|
||||
|
||||
function submit(formValues) {
|
||||
return FDO.submit(formValues).$promise;
|
||||
}
|
||||
|
||||
function configureDevice(deviceId, formValues) {
|
||||
return FDO.configureDevice({ deviceId: deviceId }, formValues).$promise;
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,34 @@ angular.module('portainer.app').service('OpenAMTService', OpenAMTServiceFactory)
|
||||
function OpenAMTServiceFactory(OpenAMT) {
|
||||
return {
|
||||
submit,
|
||||
info,
|
||||
activateDevice,
|
||||
getDevices,
|
||||
executeDeviceAction,
|
||||
enableDeviceFeatures,
|
||||
};
|
||||
|
||||
function submit(formValues) {
|
||||
return OpenAMT.submit(formValues).$promise;
|
||||
}
|
||||
|
||||
function info(endpointID) {
|
||||
return OpenAMT.info({ id: endpointID }).$promise;
|
||||
}
|
||||
|
||||
function getDevices(endpointID) {
|
||||
return OpenAMT.getDevices({ id: endpointID }).$promise;
|
||||
}
|
||||
|
||||
function executeDeviceAction(endpointID, deviceGUID, deviceAction) {
|
||||
return OpenAMT.executeDeviceAction({ id: endpointID, deviceId: deviceGUID, deviceAction: deviceAction }).$promise;
|
||||
}
|
||||
|
||||
function activateDevice(endpointID) {
|
||||
return OpenAMT.activate({ id: endpointID }).$promise;
|
||||
}
|
||||
|
||||
function enableDeviceFeatures(endpointID, deviceGUID, enabledFeatures) {
|
||||
return OpenAMT.enableDeviceFeatures({ id: endpointID, deviceId: deviceGUID, enabledFeatures: enabledFeatures }).$promise;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +207,16 @@ angular.module('portainer.app').factory('FileUploadService', [
|
||||
return $q.all(queue);
|
||||
};
|
||||
|
||||
service.uploadOwnershipVoucher = function (voucherFile) {
|
||||
return Upload.upload({
|
||||
url: 'api/fdo/register',
|
||||
data: {
|
||||
voucher: voucherFile,
|
||||
},
|
||||
ignoreLoadingBar: true,
|
||||
});
|
||||
};
|
||||
|
||||
return service;
|
||||
},
|
||||
]);
|
||||
|
||||
79
app/portainer/settings/general/fdo/fdo.controller.js
Normal file
79
app/portainer/settings/general/fdo/fdo.controller.js
Normal file
@@ -0,0 +1,79 @@
|
||||
class FDOController {
|
||||
/* @ngInject */
|
||||
constructor($async, $scope, $state, FDOService, SettingsService, Notifications) {
|
||||
Object.assign(this, { $async, $scope, $state, FDOService, SettingsService, Notifications });
|
||||
|
||||
this.formValues = {
|
||||
enabled: false,
|
||||
ownerURL: '',
|
||||
ownerUsername: '',
|
||||
ownerPassword: '',
|
||||
};
|
||||
|
||||
this.originalValues = {
|
||||
...this.formValues,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
actionInProgress: false,
|
||||
};
|
||||
|
||||
this.save = this.save.bind(this);
|
||||
this.onChangeEnableFDO = this.onChangeEnableFDO.bind(this);
|
||||
}
|
||||
|
||||
onChangeEnableFDO(checked) {
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.formValues.enabled = checked;
|
||||
});
|
||||
}
|
||||
|
||||
isFormChanged() {
|
||||
return Object.entries(this.originalValues).some(([key, value]) => value !== this.formValues[key]);
|
||||
}
|
||||
|
||||
async save() {
|
||||
return this.$async(async () => {
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
await this.FDOService.submit(this.formValues);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
this.Notifications.success(`FDO successfully ${this.formValues.enabled ? 'enabled' : 'disabled'}`);
|
||||
this.originalValues = {
|
||||
...this.formValues,
|
||||
};
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Failed applying changes');
|
||||
}
|
||||
this.state.actionInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
async $onInit() {
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
const data = await this.SettingsService.settings();
|
||||
const config = data.FDOConfiguration;
|
||||
|
||||
if (config) {
|
||||
this.formValues = {
|
||||
...this.formValues,
|
||||
enabled: config.Enabled,
|
||||
ownerURL: config.OwnerURL,
|
||||
ownerUsername: config.OwnerUsername,
|
||||
ownerPassword: config.OwnerPassword,
|
||||
};
|
||||
|
||||
this.originalValues = {
|
||||
...this.formValues,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Failed loading settings');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default FDOController;
|
||||
89
app/portainer/settings/general/fdo/fdo.html
Normal file
89
app/portainer/settings/general/fdo/fdo.html
Normal file
@@ -0,0 +1,89 @@
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-laptop" title-text="FDO"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="FDOForm">
|
||||
<por-switch-field checked="$ctrl.formValues.enabled" label="'Enable FDO Management Service'" on-change="($ctrl.onChangeEnableFDO)"></por-switch-field>
|
||||
<span class="small">
|
||||
<p class="text-muted" style="margin-top: 10px;">
|
||||
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
When enabled, this will allow Portainer to interact with FDO Services.
|
||||
</p>
|
||||
</span>
|
||||
|
||||
<div ng-show="$ctrl.formValues.enabled">
|
||||
<hr />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="owner_url" class="col-sm-3 control-label text-left">
|
||||
Owner Service Server
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="owner_url"
|
||||
name="owner_url"
|
||||
placeholder="http://127.0.0.1:8042"
|
||||
ng-model="$ctrl.formValues.ownerURL"
|
||||
ng-required="$ctrl.formValues.enabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="owner_username" class="col-sm-3 control-label text-left">
|
||||
Owner Service Username
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="owner_username"
|
||||
name="owner_username"
|
||||
placeholder="username"
|
||||
ng-model="$ctrl.formValues.ownerUsername"
|
||||
ng-required="$ctrl.formValues.enabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="owner_url" class="col-sm-3 control-label text-left">
|
||||
Owner Service Password
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="owner_password"
|
||||
name="owner_password"
|
||||
placeholder="password"
|
||||
ng-model="$ctrl.formValues.ownerPassword"
|
||||
ng-required="$ctrl.formValues.enabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="$ctrl.state.actionInProgress || !FDOForm.$valid || !$ctrl.isFormChanged()"
|
||||
ng-click="$ctrl.save()"
|
||||
button-spinner="$ctrl.state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="$ctrl.state.actionInProgress">Save Settings</span>
|
||||
<span ng-show="$ctrl.state.actionInProgress">In progress...</span>
|
||||
</button>
|
||||
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
6
app/portainer/settings/general/fdo/index.js
Normal file
6
app/portainer/settings/general/fdo/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import controller from './fdo.controller.js';
|
||||
|
||||
export const fdo = {
|
||||
templateUrl: './fdo.html',
|
||||
controller,
|
||||
};
|
||||
@@ -2,5 +2,10 @@ import angular from 'angular';
|
||||
|
||||
import { sslCertificate } from './ssl-certificate';
|
||||
import { openAMT } from './open-amt';
|
||||
import { fdo } from './fdo';
|
||||
|
||||
export default angular.module('portainer.settings.general', []).component('sslCertificateSettings', sslCertificate).component('openAmtSettings', openAMT).name;
|
||||
export default angular
|
||||
.module('portainer.settings.general', [])
|
||||
.component('sslCertificateSettings', sslCertificate)
|
||||
.component('openAmtSettings', openAMT)
|
||||
.component('fdoSettings', fdo).name;
|
||||
|
||||
@@ -11,12 +11,8 @@ class OpenAmtController {
|
||||
mpsPassword: '',
|
||||
domainName: '',
|
||||
certFile: null,
|
||||
certFileText: '',
|
||||
certPassword: '',
|
||||
useWirelessConfig: false,
|
||||
wifiAuthenticationMethod: '4',
|
||||
wifiEncryptionMethod: '3',
|
||||
wifiSsid: '',
|
||||
wifiPskPass: '',
|
||||
};
|
||||
|
||||
this.originalValues = {
|
||||
@@ -42,41 +38,57 @@ class OpenAmtController {
|
||||
}
|
||||
|
||||
isFormValid() {
|
||||
return !this.formValues.enableOpenAMT || this.formValues.certFile != null;
|
||||
if (!this.formValues.enableOpenAMT) {
|
||||
return true;
|
||||
}
|
||||
return this.formValues.certFile != null || this.formValues.certFileText !== '';
|
||||
}
|
||||
|
||||
async readFile() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = this.formValues.certFile;
|
||||
onCertFileSelected(file) {
|
||||
return this.$scope.$evalAsync(async () => {
|
||||
if (file) {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.fileName = file.name;
|
||||
fileReader.onload = (e) => {
|
||||
const base64 = e.target.result;
|
||||
// remove prefix of "data:application/x-pkcs12;base64," returned by "readAsDataURL()"
|
||||
const index = base64.indexOf('base64,');
|
||||
const cert = base64.substring(index + 7, base64.length);
|
||||
resolve(cert);
|
||||
};
|
||||
fileReader.onerror = () => {
|
||||
reject(new Error('error reading provisioning certificate file'));
|
||||
};
|
||||
fileReader.readAsDataURL(file);
|
||||
this.formValues.certFileText = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async readFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.fileName = file.name;
|
||||
fileReader.onload = (e) => {
|
||||
const base64 = e.target.result;
|
||||
// remove prefix of "data:application/x-pkcs12;base64," returned by "readAsDataURL()"
|
||||
const index = base64.indexOf('base64,');
|
||||
const cert = base64.substring(index + 7, base64.length);
|
||||
resolve(cert);
|
||||
};
|
||||
fileReader.onerror = () => {
|
||||
reject(new Error('error reading provisioning certificate file'));
|
||||
};
|
||||
fileReader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
async save() {
|
||||
return this.$async(async () => {
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
this.formValues.certFileText = this.formValues.certFile ? await this.readFile(this.formValues.certFile) : null;
|
||||
if (this.formValues.certFile) {
|
||||
this.formValues.certFileText = await this.readFile(this.formValues.certFile);
|
||||
}
|
||||
await this.OpenAMTService.submit(this.formValues);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
this.Notifications.success(`OpenAMT successfully ${this.formValues.enableOpenAMT ? 'enabled' : 'disabled'}`);
|
||||
this.originalValues = {
|
||||
...this.formValues,
|
||||
};
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Failed applying changes');
|
||||
if (this.originalValues.certFileText === '') {
|
||||
this.formValues.certFileText = '';
|
||||
}
|
||||
}
|
||||
this.state.actionInProgress = false;
|
||||
});
|
||||
@@ -94,16 +106,12 @@ class OpenAmtController {
|
||||
enableOpenAMT: config.Enabled,
|
||||
mpsServer: config.MPSServer,
|
||||
mpsUser: config.Credentials.MPSUser,
|
||||
mpsPassword: config.Credentials.MPSPassword,
|
||||
domainName: config.DomainConfiguration.DomainName,
|
||||
certPassword: config.DomainConfiguration.CertPassword,
|
||||
certFileText: config.DomainConfiguration.CertFileText,
|
||||
};
|
||||
|
||||
if (config.WirelessConfiguration) {
|
||||
this.formValues.useWirelessConfig = true;
|
||||
this.formValues.wifiAuthenticationMethod = config.WirelessConfiguration.AuthenticationMethod;
|
||||
this.formValues.wifiEncryptionMethod = config.WirelessConfiguration.EncryptionMethod;
|
||||
this.formValues.wifiSsid = config.WirelessConfiguration.SSID;
|
||||
}
|
||||
|
||||
this.originalValues = {
|
||||
...this.formValues,
|
||||
};
|
||||
|
||||
@@ -127,12 +127,20 @@
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<button style="margin-left: 0px !important;" class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.formValues.certFile" ngf-pattern=".pfx" name="certFile">
|
||||
<button
|
||||
style="margin-left: 0px !important;"
|
||||
class="btn btn-sm btn-primary"
|
||||
ngf-select="$ctrl.onCertFileSelected($file)"
|
||||
ng-model="$ctrl.formValues.certFile"
|
||||
ngf-pattern=".pfx"
|
||||
name="certFile"
|
||||
>
|
||||
Upload file
|
||||
</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ $ctrl.formValues.certFile.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!$ctrl.formValues.certFile" aria-hidden="true"></i>
|
||||
<i class="fa fa-check green-icon" ng-show="$ctrl.formValues.certFileText && !$ctrl.state.actionInProgress" aria-hidden="true"></i>
|
||||
<i class="fa fa-times red-icon" ng-show="!$ctrl.formValues.certFileText && !$ctrl.formValues.certFile" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,69 +179,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<!-- <por-switch-field checked="$ctrl.formValues.useWirelessConfig" label="'Wireless Configuration'" on-change="$ctrl.onChangeUseWirelessConfig"></por-switch-field> -->
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="$ctrl.formValues.useWirelessConfig">
|
||||
<div class="form-group">
|
||||
<label for="wifi_auth_method" class="col-sm-3 control-label text-left">
|
||||
Authentication Method
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<select class="form-control" ng-model="$ctrl.formValues.wifiAuthenticationMethod">
|
||||
<option value="4">WPA PSK</option>
|
||||
<option value="6">WPA2 PSK</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="wifi_encrypt_method" class="col-sm-3 control-label text-left">
|
||||
Encryption Method
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<select class="form-control" ng-model="$ctrl.formValues.wifiEncryptionMethod" id="wifi_encrypt_method">
|
||||
<option value="3">TKIP</option>
|
||||
<option value="4">CCMP</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="wifi_ssid" class="col-sm-3 control-label text-left">
|
||||
SSID
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="$ctrl.formValues.wifiSsid"
|
||||
id="wifi_ssid"
|
||||
placeholder="SSIID"
|
||||
ng-required="$ctrl.formValues.enableOpenAMT && $ctrl.formValues.useWirelessConfig"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="wifi_pass" class="col-sm-3 control-label text-left">
|
||||
PSK Passphrase
|
||||
<portainer-tooltip position="bottom" message="PSK Passphrase length should be greater than or equal to 8 and less than or equal to 63"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
ng-model="$ctrl.formValues.wifiPskPass"
|
||||
id="wifi_pass"
|
||||
placeholder="******"
|
||||
ng-required="$ctrl.formValues.enableOpenAMT && $ctrl.formValues.useWirelessConfig"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
201
app/portainer/views/devices/import/importDevice.html
Normal file
201
app/portainer/views/devices/import/importDevice.html
Normal file
@@ -0,0 +1,201 @@
|
||||
<rd-header>
|
||||
<rd-header-title title-text="FDO Device Configuration"></rd-header-title>
|
||||
<rd-header-content> <a ui-sref="portainer.endpoints">Environments</a> > Import FDO Device </rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-magic" title-text="Import Device Set up"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="fdoForm">
|
||||
<!-- info -->
|
||||
<span class="small">
|
||||
<p class="text-muted" style="margin-top: 10px;">
|
||||
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
You are setting up a Portainer Edge Agent that will initiate the communications with the Portainer instance and your FDO Devices.
|
||||
</p>
|
||||
</span>
|
||||
<!-- !info -->
|
||||
<!-- import voucher -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Import Voucher
|
||||
</div>
|
||||
<div>
|
||||
<div class="form-group" ng-show="!state.voucherUploaded">
|
||||
<span class="small col-sm-12">
|
||||
<p class="text-muted" style="margin-top: 10px;">
|
||||
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Import your Manufacturer's Owner Voucher to initiate device attestation. <a href="/" target="_blank">Link here</a>
|
||||
</p>
|
||||
</span>
|
||||
<div class="col-sm-8">
|
||||
<button
|
||||
style="margin-left: 0px !important;"
|
||||
class="btn btn-sm btn-primary"
|
||||
ngf-select="onVoucherFileChange($file)"
|
||||
ng-model="formValues.VoucherFile"
|
||||
name="voucherFile"
|
||||
ng-disabled="state.voucherUploading"
|
||||
button-spinner="state.voucherUploading"
|
||||
>
|
||||
<span ng-hide="state.voucherUploading">Upload <i class="fa fa-upload" aria-hidden="true" style="margin-left: 5px;"></i></span>
|
||||
<span ng-show="state.voucherUploading">Uploading Voucher...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="state.voucherUploading">
|
||||
<div class="col-sm-12 small text-success">
|
||||
<p>Connecting to Manufacturer's Rendezvous Server...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="state.voucherUploaded">
|
||||
<div class="col-sm-12">
|
||||
<p>Ownership Voucher Uploaded <i class="fa fa-check green-icon" aria-hidden="true" style="margin-left: 5px;"></i></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="fdoForm.voucherFile.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="fdoForm.voucherFile.$error">
|
||||
<p ng-message="pattern"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> File type is invalid.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !import voucher -->
|
||||
<!-- device details -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Device details
|
||||
</div>
|
||||
<div>
|
||||
<span class="small">
|
||||
<p class="text-muted" style="margin-top: 10px;">
|
||||
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Profile name will serve as your reference name in Portainer
|
||||
</p>
|
||||
</span>
|
||||
<!-- device name input -->
|
||||
<div class="form-group">
|
||||
<label for="device_name" class="col-sm-3 col-lg-2 control-label text-left">Device Name</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="device_name"
|
||||
placeholder="e.g. FDO-Test01"
|
||||
ng-model="formValues.DeviceName"
|
||||
ng-required="state.voucherUploaded"
|
||||
ng-disabled="!state.voucherUploaded"
|
||||
auto-focus
|
||||
data-cy="deviceImport-deviceNameInput"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="fdoForm.device_name.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="fdoForm.device_name.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !device name input -->
|
||||
<!-- portainer-instance-input -->
|
||||
<div class="form-group">
|
||||
<label for="endpoint_url" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Portainer server URL
|
||||
<portainer-tooltip position="bottom" message="URL of the Portainer instance that the agent will use to initiate the communications."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="endpoint_url"
|
||||
ng-model="formValues.PortainerURL"
|
||||
ng-required="state.voucherUploaded"
|
||||
ng-disabled="!state.voucherUploaded"
|
||||
placeholder="e.g. http://10.0.0.10:9443"
|
||||
required
|
||||
data-cy="deviceImport-portainerServerUrlInput"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="endpointCreationForm.endpoint_url.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="endpointCreationForm.endpoint_url.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !portainer-instance-input -->
|
||||
</div>
|
||||
<!-- device profile input -->
|
||||
<div class="form-group">
|
||||
<label for="device_profile" class="col-sm-3 col-lg-2 control-label text-left">Device Profile</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<select id="device_profile" ng-model="formValues.DeviceProfile" class="form-control" ng-disabled="!state.voucherUploaded">
|
||||
<option selected disabled hidden value="">Select a profile for your device</option>
|
||||
<option ng-repeat="profile in profiles | orderBy: 'Name'" ng-value="profile.Id">{{ profile.Name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !device profile input -->
|
||||
<!-- !device details -->
|
||||
<!-- tags -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Set up Tags
|
||||
</div>
|
||||
<div>
|
||||
<span class="small">
|
||||
<p class="text-muted" style="margin-top: 10px;">
|
||||
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This is just an option if your device is under a certain group <a href="/" target="_blank">Link here</a>
|
||||
</p>
|
||||
</span>
|
||||
<!-- group -->
|
||||
<div class="form-group">
|
||||
<label for="device_group" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Group
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<select
|
||||
class="form-control"
|
||||
ng-options="group.Id as group.Name for group in groups"
|
||||
ng-model="formValues.GroupId"
|
||||
id="device_group"
|
||||
ng-required="state.voucherUploaded"
|
||||
ng-disabled="!state.voucherUploaded"
|
||||
data-cy="deviceImport-deviceGroup"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !group -->
|
||||
<!-- tags -->
|
||||
<div class="form-group">
|
||||
<tag-selector ng-if="formValues && availableTags" tags="availableTags" model="formValues.TagIds" allow-create="state.allowCreateTag" on-create="(onCreateTag)">
|
||||
</tag-selector>
|
||||
</div>
|
||||
<!-- !tags -->
|
||||
<!-- actions -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-click="configureDevice()"
|
||||
ng-disabled="state.actionInProgress || !state.voucherUploaded || !fdoForm.$valid"
|
||||
button-spinner="state.actionInProgress"
|
||||
data-cy="deviceImport-saveDeviceButton"
|
||||
>
|
||||
<span ng-hide="state.actionInProgress">Save Configuration</span>
|
||||
<span ng-show="state.actionInProgress">Saving...</span>
|
||||
</button>
|
||||
<a type="button" class="btn btn-default btn-sm" ui-sref="portainer.endpoints">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
130
app/portainer/views/devices/import/importDeviceController.js
Normal file
130
app/portainer/views/devices/import/importDeviceController.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import { PortainerEndpointCreationTypes } from 'Portainer/models/endpoint/models';
|
||||
|
||||
angular
|
||||
.module('portainer.app')
|
||||
.controller('ImportDeviceController', function ImportDeviceController(
|
||||
$async,
|
||||
$q,
|
||||
$scope,
|
||||
$state,
|
||||
EndpointService,
|
||||
GroupService,
|
||||
TagService,
|
||||
Notifications,
|
||||
Authentication,
|
||||
FileUploadService,
|
||||
FDOService
|
||||
) {
|
||||
$scope.state = {
|
||||
actionInProgress: false,
|
||||
voucherUploading: false,
|
||||
voucherUploaded: false,
|
||||
deviceID: '',
|
||||
allowCreateTag: Authentication.isAdmin(),
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
DeviceName: '',
|
||||
DeviceProfile: '',
|
||||
GroupId: 1,
|
||||
TagIds: [],
|
||||
VoucherFile: null,
|
||||
PortainerURL: '',
|
||||
};
|
||||
|
||||
$scope.profiles = [{ Id: 1, Name: 'Docker Standalone + Edge Agent' }];
|
||||
|
||||
$scope.onVoucherFileChange = function (file) {
|
||||
if (file) {
|
||||
$scope.state.voucherUploading = true;
|
||||
|
||||
FileUploadService.uploadOwnershipVoucher(file)
|
||||
.then(function success(response) {
|
||||
$scope.state.voucherUploading = false;
|
||||
$scope.state.voucherUploaded = true;
|
||||
$scope.deviceID = response.data.guid;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
$scope.state.voucherUploading = false;
|
||||
Notifications.error('Failure', err, 'Unable to upload Ownership Voucher');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.onCreateTag = function onCreateTag(tagName) {
|
||||
return $async(onCreateTagAsync, tagName);
|
||||
};
|
||||
|
||||
async function onCreateTagAsync(tagName) {
|
||||
try {
|
||||
const tag = await TagService.createTag(tagName);
|
||||
$scope.availableTags = $scope.availableTags.concat(tag);
|
||||
$scope.formValues.TagIds = $scope.formValues.TagIds.concat(tag.Id);
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to create tag');
|
||||
}
|
||||
}
|
||||
|
||||
$scope.configureDevice = function () {
|
||||
return $async(async () => {
|
||||
$scope.state.actionInProgress = true;
|
||||
|
||||
try {
|
||||
var endpoint = await EndpointService.createRemoteEndpoint(
|
||||
$scope.formValues.DeviceName,
|
||||
PortainerEndpointCreationTypes.EdgeAgentEnvironment,
|
||||
$scope.formValues.PortainerURL,
|
||||
'',
|
||||
$scope.formValues.GroupId,
|
||||
$scope.formValues.TagIds,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to create the environment');
|
||||
return;
|
||||
} finally {
|
||||
$scope.state.actionInProgress = false;
|
||||
}
|
||||
|
||||
const config = {
|
||||
edgekey: endpoint.EdgeKey,
|
||||
name: $scope.formValues.DeviceName,
|
||||
profile: $scope.formValues.DeviceProfile,
|
||||
};
|
||||
|
||||
try {
|
||||
await FDOService.configureDevice($scope.deviceID, config);
|
||||
Notifications.success('Device successfully imported');
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to import device');
|
||||
return;
|
||||
} finally {
|
||||
$scope.state.actionInProgress = false;
|
||||
}
|
||||
|
||||
$state.go('portainer.home');
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
$q.all({
|
||||
groups: GroupService.groups(),
|
||||
tags: TagService.tags(),
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.groups = data.groups;
|
||||
$scope.availableTags = data.tags;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to load groups');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
});
|
||||
@@ -214,6 +214,79 @@
|
||||
<por-endpoint-security form-data="formValues.SecurityFormData" endpoint="endpoint"></por-endpoint-security>
|
||||
</div>
|
||||
<!-- !endpoint-security -->
|
||||
<!-- open-amt info -->
|
||||
<div ng-if="state.showAMTInfo">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Open Active Management Technology
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="endpoint_managementinfoVersion" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
AMT Version
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" ng-disabled="true" class="form-control" id="endpoint_managementinfoVersion" ng-model="endpoint.ManagementInfo['AMT']" placeholder="Loading..." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="endpoint_managementinfoUUID" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
UUID
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" ng-disabled="true" class="form-control" id="endpoint_managementinfoUUID" ng-model="endpoint.ManagementInfo['UUID']" placeholder="Loading..." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="endpoint_managementinfoBuildNumber" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Build Number
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
ng-disabled="true"
|
||||
class="form-control"
|
||||
id="endpoint_managementinfoBuildNumber"
|
||||
ng-model="endpoint.ManagementInfo['Build Number']"
|
||||
placeholder="Loading..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="endpoint_managementinfoControlMode" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Control Mode
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
ng-disabled="true"
|
||||
class="form-control"
|
||||
id="endpoint_managementinfoControlMode"
|
||||
ng-model="endpoint.ManagementInfo['Control Mode']"
|
||||
placeholder="Loading..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="endpoint_managementinfoDNSSuffix" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
DNS Suffix
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
ng-disabled="true"
|
||||
class="form-control"
|
||||
id="endpoint_managementinfoDNSSuffix"
|
||||
ng-model="endpoint.ManagementInfo['DNS Suffix']"
|
||||
placeholder="Loading..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !open-amt info -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
|
||||
@@ -4,6 +4,7 @@ import uuidv4 from 'uuid/v4';
|
||||
import { PortainerEndpointTypes } from '@/portainer/models/endpoint/models';
|
||||
import { EndpointSecurityFormData } from '@/portainer/components/endpointSecurity/porEndpointSecurityModel';
|
||||
import { getAgentShortVersion } from 'Portainer/views/endpoints/helpers';
|
||||
import EndpointHelper from '@/portainer/helpers/endpointHelper';
|
||||
|
||||
angular.module('portainer.app').controller('EndpointController', EndpointController);
|
||||
|
||||
@@ -23,7 +24,8 @@ function EndpointController(
|
||||
Authentication,
|
||||
SettingsService,
|
||||
ModalService,
|
||||
StateManager
|
||||
StateManager,
|
||||
OpenAMTService
|
||||
) {
|
||||
const DEPLOYMENT_TABS = {
|
||||
SWARM: 'swarm',
|
||||
@@ -65,6 +67,7 @@ function EndpointController(
|
||||
{ key: '1 day', value: 86400 },
|
||||
],
|
||||
allowSelfSignedCerts: true,
|
||||
showAMTInfo: false,
|
||||
};
|
||||
|
||||
$scope.agentVersion = StateManager.getState().application.version;
|
||||
@@ -278,12 +281,44 @@ function EndpointController(
|
||||
$scope.availableTags = tags;
|
||||
|
||||
configureState();
|
||||
|
||||
const disconnectedEdge = $scope.state.edgeEndpoint && !endpoint.EdgeID;
|
||||
if (EndpointHelper.isDockerEndpoint(endpoint) && !disconnectedEdge) {
|
||||
const featureFlagValue = settings && settings.FeatureFlagSettings && settings.FeatureFlagSettings['open-amt'];
|
||||
const featureEnabled = settings && settings.OpenAMTConfiguration && settings.OpenAMTConfiguration.Enabled;
|
||||
$scope.state.showAMTInfo = featureFlagValue && featureEnabled;
|
||||
}
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve environment details');
|
||||
}
|
||||
|
||||
if ($scope.state.showAMTInfo) {
|
||||
try {
|
||||
$scope.endpoint.ManagementInfo = {};
|
||||
const [amtInfo] = await Promise.all([OpenAMTService.info($transition$.params().id)]);
|
||||
|
||||
try {
|
||||
$scope.endpoint.ManagementInfo = JSON.parse(amtInfo.RawOutput);
|
||||
} catch (err) {
|
||||
console.log('Failure', err, 'Unable to JSON parse AMT info: ' + amtInfo.RawOutput);
|
||||
clearAMTManagementInfo(amtInfo.RawOutput);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Failure', err);
|
||||
clearAMTManagementInfo('Unable to retrieve AMT environment details');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearAMTManagementInfo(versionValue) {
|
||||
$scope.endpoint.ManagementInfo['AMT'] = versionValue;
|
||||
$scope.endpoint.ManagementInfo['UUID'] = '-';
|
||||
$scope.endpoint.ManagementInfo['Control Mode'] = '-';
|
||||
$scope.endpoint.ManagementInfo['Build Number'] = '-';
|
||||
$scope.endpoint.ManagementInfo['DNS Suffix'] = '-';
|
||||
}
|
||||
|
||||
function buildLinuxStandaloneCommand(agentVersion, agentShortVersion, edgeId, edgeKey, allowSelfSignedCerts) {
|
||||
return `
|
||||
docker run -d \\
|
||||
|
||||
@@ -7,7 +7,24 @@
|
||||
<rd-header-content>Environment management</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div
|
||||
class="row"
|
||||
style="width: 100%; height: 100%; text-align: center; display: flex; flex-direction: column; align-items: center; justify-content: center;"
|
||||
ng-if="state.loadingMessage"
|
||||
>
|
||||
<div class="sk-fold">
|
||||
<div class="sk-fold-cube"></div>
|
||||
<div class="sk-fold-cube"></div>
|
||||
<div class="sk-fold-cube"></div>
|
||||
<div class="sk-fold-cube"></div>
|
||||
</div>
|
||||
<span style="margin-top: 25px;">
|
||||
{{ state.loadingMessage }}
|
||||
<i class="fa fa-cog fa-spin"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="!state.loadingMessage">
|
||||
<div class="col-sm-12">
|
||||
<endpoints-datatable
|
||||
title-text="Environments"
|
||||
@@ -16,6 +33,7 @@
|
||||
order-by="Name"
|
||||
remove-action="removeAction"
|
||||
retrieve-page="getPaginatedEndpoints"
|
||||
set-loading-message="setLoadingMessage"
|
||||
></endpoints-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,8 +4,16 @@ import EndpointHelper from 'Portainer/helpers/endpointHelper';
|
||||
angular.module('portainer.app').controller('EndpointsController', EndpointsController);
|
||||
|
||||
function EndpointsController($q, $scope, $state, $async, EndpointService, GroupService, ModalService, Notifications, EndpointProvider, StateManager) {
|
||||
$scope.removeAction = removeAction;
|
||||
$scope.state = {
|
||||
loadingMessage: '',
|
||||
};
|
||||
|
||||
$scope.setLoadingMessage = setLoadingMessage;
|
||||
function setLoadingMessage(message) {
|
||||
$scope.state.loadingMessage = message;
|
||||
}
|
||||
|
||||
$scope.removeAction = removeAction;
|
||||
function removeAction(endpoints) {
|
||||
ModalService.confirmDeletion('This action will remove all configurations associated to your environment(s). Continue?', (confirmed) => {
|
||||
if (!confirmed) {
|
||||
|
||||
18
app/portainer/views/endpoints/kvm/KVMControl.css
Normal file
18
app/portainer/views/endpoints/kvm/KVMControl.css
Normal file
@@ -0,0 +1,18 @@
|
||||
.canvas-container .header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.kvm-maximized {
|
||||
position: fixed;
|
||||
background: #000;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
z-index: 1000;
|
||||
max-height: 100% !important;
|
||||
}
|
||||
|
||||
45
app/portainer/views/endpoints/kvm/KVMControl.tsx
Normal file
45
app/portainer/views/endpoints/kvm/KVMControl.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useEffect, createRef } from 'react';
|
||||
import { KVM } from '@open-amt-cloud-toolkit/ui-toolkit-react/reactjs/src/kvm.bundle';
|
||||
|
||||
import { react2angular } from '@/react-tools/react2angular';
|
||||
|
||||
import './KVMControl.css';
|
||||
|
||||
export interface KVMControlProps {
|
||||
deviceId: string;
|
||||
server: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export function KVMControl({ deviceId, server, token }: KVMControlProps) {
|
||||
const divRef = createRef<HTMLInputElement>();
|
||||
useEffect(() => {
|
||||
if (divRef.current) {
|
||||
const connectButton = divRef.current.querySelector('button');
|
||||
if (connectButton) {
|
||||
connectButton.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!deviceId || !server || !token) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<div ref={divRef}>
|
||||
<KVM
|
||||
deviceId={deviceId}
|
||||
mpsServer={`https://${server}/mps/ws/relay`}
|
||||
authToken={token}
|
||||
mouseDebounceTime="200"
|
||||
canvasHeight="100%"
|
||||
canvasWidth="100%"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const KVMControlAngular = react2angular(KVMControl, [
|
||||
'deviceId',
|
||||
'server',
|
||||
'token',
|
||||
]);
|
||||
29
app/portainer/views/endpoints/kvm/endpointKVM.html
Normal file
29
app/portainer/views/endpoints/kvm/endpointKVM.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<rd-header>
|
||||
<rd-header-title title-text="KVM Control"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="portainer.endpoints">Environments</a> > <a ui-sref="portainer.endpoints.endpoint({id: $state.endpoint.Id})">{{ $state.endpoint.Name }}</a> >
|
||||
{{ $state.deviceName }} > KVM Control
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="datatable" ng-class="{ 'kvm-maximized': $state.maximized }" >
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle">KVM Control</div>
|
||||
<i class="interactive fa fa-times fa-lg pull-right" aria-hidden="true" ui-sref="portainer.endpoints"></i>
|
||||
<i ng-click="maximize()" title="Maximize" class="interactive fa fa-expand fa-lg pull-right" style="margin-right: 10px" aria-hidden="true" ></i>
|
||||
<i ng-click="minimize()" title="Minimize" class="interactive fa fa-window-minimize fa-lg pull-right" style="margin-right: 10px" aria-hidden="true" ></i>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<kvm-control device-id="$state.deviceId" server="$state.mpsServer" token="$state.mpsToken"> </kvm-control>
|
||||
</div>
|
||||
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
52
app/portainer/views/endpoints/kvm/endpointKVMController.js
Normal file
52
app/portainer/views/endpoints/kvm/endpointKVMController.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import angular from 'angular';
|
||||
|
||||
class EndpointKVMController {
|
||||
/* @ngInject */
|
||||
constructor($state, $scope, $transition$, EndpointService, OpenAMTService, Notifications) {
|
||||
this.$state = $state;
|
||||
this.$transition$ = $transition$;
|
||||
this.OpenAMTService = OpenAMTService;
|
||||
this.Notifications = Notifications;
|
||||
this.EndpointService = EndpointService;
|
||||
|
||||
this.$state.maximized = false;
|
||||
this.$state.endpointId = $transition$.params().id;
|
||||
this.$state.deviceId = $transition$.params().deviceId;
|
||||
this.$state.deviceName = $transition$.params().deviceName;
|
||||
|
||||
$scope.maximize = function() {
|
||||
this.$state.maximized = true;
|
||||
}
|
||||
|
||||
$scope.minimize = function() {
|
||||
this.$state.maximized = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async $onInit() {
|
||||
try {
|
||||
this.$state.endpoint = await this.EndpointService.endpoint(this.$state.endpointId);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve environment information');
|
||||
}
|
||||
|
||||
try {
|
||||
const featuresPayload = {
|
||||
IDER: true,
|
||||
KVM: true,
|
||||
SOL: true,
|
||||
redirection: true,
|
||||
userConsent: 'none',
|
||||
};
|
||||
const mpsAuthorization = await this.OpenAMTService.enableDeviceFeatures(this.$state.endpointId, this.$state.deviceId, featuresPayload);
|
||||
this.$state.mpsServer = mpsAuthorization.Server;
|
||||
this.$state.mpsToken = mpsAuthorization.Token;
|
||||
} catch (e) {
|
||||
this.Notifications.error('Failure', e, `Failed to load kvm for device`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default EndpointKVMController;
|
||||
angular.module('portainer.app').controller('EndpointKVMController', EndpointKVMController);
|
||||
5
app/portainer/views/endpoints/kvm/index.js
Normal file
5
app/portainer/views/endpoints/kvm/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { KVMControlAngular } from '@/portainer/views/endpoints/kvm/KVMControl';
|
||||
|
||||
angular.module('portainer.app').component('kvmControl', KVMControlAngular).name;
|
||||
@@ -101,8 +101,7 @@ angular
|
||||
});
|
||||
|
||||
try {
|
||||
const tags = TagService.tags();
|
||||
$scope.tags = tags;
|
||||
$scope.tags = await TagService.tags();
|
||||
} catch (err) {
|
||||
Notifications.error('Failed loading page data', err);
|
||||
}
|
||||
|
||||
@@ -186,6 +186,8 @@
|
||||
|
||||
<open-amt-settings ng-if="settings.FeatureFlagSettings && settings.FeatureFlagSettings['open-amt']"></open-amt-settings>
|
||||
|
||||
<fdo-settings ng-if="settings.FeatureFlagSettings && settings.FeatureFlagSettings['fdo']"></fdo-settings>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"@aws-crypto/sha256-js": "^2.0.0",
|
||||
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||
"@nxmix/tokenize-ansi": "^3.0.0",
|
||||
"@open-amt-cloud-toolkit/ui-toolkit-react": "2.0.0",
|
||||
"@uirouter/angularjs": "1.0.11",
|
||||
"@uirouter/react": "^1.0.7",
|
||||
"@uirouter/react-hybrid": "^1.0.4",
|
||||
|
||||
Reference in New Issue
Block a user