Compare commits

...

25 Commits

Author SHA1 Message Date
cheloRydel
d85d5e272d feat(openamt): use Intel's image for RPC 2021-12-20 14:58:25 -03:00
cheloRydel
f8c1f6ee11 merge develop 2021-12-20 10:15:40 -03:00
Marcelo Rydel
e1f7411926 feat(openamt): change kvm redirection for pop up, always enable features [INT-37] (#6293) 2021-12-17 17:07:58 -03:00
Marcelo Rydel
2fcd238320 feat(openamt): change kvm redirection for pop up, always enable features [INT-37] (#6292) 2021-12-17 12:35:48 -03:00
cheloRydel
24b5fce26d yarn install 2021-12-17 11:00:59 -03:00
Marcelo Rydel
ea49a192da feat(openamt): Remove wireless config related code [INT-41] (#6291) 2021-12-16 16:26:38 -03:00
Marcelo Rydel
11e486019a feat(openamt): Better UI/UX for AMT activation loading [INT-39] (#6290) 2021-12-16 16:01:49 -03:00
Marcelo Rydel
1af028df4f Merge branch 'develop' into feat/poc-intel 2021-12-16 09:32:43 -03:00
Marcelo Rydel
a84ec025e8 feat(openamt): preload existing AMT settings (#6283) 2021-12-16 09:29:03 -03:00
Marcelo Rydel
6dfe8ad97a fix(intel): Fix switches params (#6282) 2021-12-15 10:05:04 -03:00
Marcelo Rydel
a6d9e566ba feat(openamt): Do not fetch OpenAMT details for an unassociated Edge endpoint (#6273) 2021-12-15 09:32:31 -03:00
deviantony
867168cac7 refactor(fdo): fix develop merge issues 2021-12-15 10:07:00 +00:00
Anthony Lapenna
184db846c2 Merge branch 'develop' into feat/poc-intel 2021-12-15 09:54:52 +00:00
Marcelo Rydel
738ec4316d feat(fdo): add import device UI [INT-20] (#6240)
feat(fdo): add import device UI INT-20
2021-12-14 19:51:16 -03:00
Anthony Lapenna
8567c4051a Merge branch 'develop' into feat/poc-intel 2021-12-13 07:27:19 +00:00
Marcelo Rydel
415af981f8 feat(openamt): Disable the ability to use KVM and OOB actions on a MPS disconnected device [INT-36] (#6254) 2021-12-10 17:05:38 -03:00
Marcelo Rydel
3acaee1489 feat(openamt): Increase OpenAMT timeouts [INT-30] (#6253) 2021-12-11 07:45:12 +13:00
Marcelo Rydel
27ced894fd feat(openamt): hide wireless config in OpenAMT form (#6250) 2021-12-09 17:15:57 -03:00
andres-portainer
cdf954a5e5 feat(fdo): implement Owner client INT-17 (#6231)
feat(fdo): implement Owner client INT-17
2021-12-08 19:33:23 -03:00
andres-portainer
dbe17b9425 feat(fdo): implement the FDO configuration settings INT-19 (#6238)
feat(fdo): implement the FDO configuration settings INT-19
2021-12-08 15:08:42 -03:00
Marcelo Rydel
b36a0ec258 feat(openamt): Enable KVM by default [INT-25] (#6228) 2021-12-07 09:14:16 -03:00
Marcelo Rydel
7ddea7e09e feat(openamt): Enhance the Environments MX to activate OpenAMT on compatible environments [INT-7] (#6196) 2021-12-03 15:27:52 -03:00
Marcelo Rydel
4173702662 feat(openamt): add AMT Devices KVM Connection [INT-10] (#6179) 2021-12-03 13:00:59 -03:00
Marcelo Rydel
11268e7816 feat(openamt): add AMT Devices Ouf of Band Managamenet actions [INT-9] (#6171) 2021-12-03 12:44:51 -03:00
Marcelo Rydel
e2bb76ff58 feat(openamt): add AMT Devices information in Environments view [INT-8] (#6169) 2021-12-03 12:26:53 -03:00
69 changed files with 5525 additions and 3712 deletions

View File

@@ -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
}

View File

@@ -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
View 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
}

View 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
}

View File

@@ -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

View File

@@ -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=

View File

@@ -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
}

View 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
}

View File

@@ -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)

View File

@@ -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
}

View 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)
}

View 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
}

View File

@@ -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
}

View File

@@ -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"):

View 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)
}

View 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)
}

View 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
}

View 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)
}

View 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})
}

View 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)
}

View File

@@ -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
}
}

View 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)
}

View 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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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)
})
}
}
}

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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
View File

@@ -6,3 +6,5 @@ declare module '*.png' {
}
declare module '*.css';
declare module '@open-amt-cloud-toolkit/ui-toolkit-react/reactjs/src/kvm.bundle';

View File

@@ -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);

View File

@@ -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>

View File

@@ -0,0 +1,9 @@
angular.module('portainer.docker').component('amtDevicesDatatable', {
templateUrl: './amtDevicesDatatable.html',
controller: 'AMTDevicesDatatableController',
bindings: {
endpointId: '<',
devices: '<',
error: '<',
},
});

View File

@@ -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();
};
},
]);

View File

@@ -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>

View File

@@ -9,5 +9,6 @@ angular.module('portainer.app').component('endpointsDatatable', {
reverseOrder: '<',
removeAction: '<',
retrievePage: '<',
setLoadingMessage: '<',
},
});

View File

@@ -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();
};
},

View File

@@ -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,

View File

@@ -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
View 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' } },
}
);
}

View File

@@ -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' },
},
}
);
}

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
},
]);

View 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;

View 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>

View File

@@ -0,0 +1,6 @@
import controller from './fdo.controller.js';
export const fdo = {
templateUrl: './fdo.html',
controller,
};

View File

@@ -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;

View File

@@ -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,
};

View File

@@ -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 />

View 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> &gt; 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>

View 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();
});

View File

@@ -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

View File

@@ -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 \\

View File

@@ -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>

View File

@@ -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) {

View 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;
}

View 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',
]);

View 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> &gt; <a ui-sref="portainer.endpoints.endpoint({id: $state.endpoint.Id})">{{ $state.endpoint.Name }}</a> &gt;
{{ $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>

View 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);

View 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;

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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",

6156
yarn.lock

File diff suppressed because it is too large Load Diff