Compare commits

...

25 Commits

Author SHA1 Message Date
Sven Dowideit
6d3c1396e7 WIP: initial POC using a container to call rpc-go to show on the environment page
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2021-11-25 16:45:39 +10:00
Sven Dowideit
d13f3e7548 chore(build): add a PORTAINER_FLAGS env var for yarn so I can default a password, and enable feature flags
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2021-11-25 16:43:35 +10:00
cheloRydel
4e006e21de feat(openamt): review, rename package 2021-11-24 19:13:40 -03:00
cheloRydel
81f3527556 remove comma 2021-11-24 17:04:18 -03:00
cheloRydel
7577a13589 fix(openamt): fix to allow PATCH on amt domain, use static password for amt profile 2021-11-24 17:00:58 -03:00
cheloRydel
21f6292582 testing: remove TODOs 2021-11-23 10:34:38 -03:00
cheloRydel
bfcd6a3592 testing: activate flag for deployment 2021-11-23 09:42:06 -03:00
cheloRydel
6a0f3ac618 fix(openamt): check for null config, determine cira address format value from url 2021-11-22 16:24:09 -03:00
cheloRydel
ecab23e168 feat(openamt): sort packages 2021-11-19 11:31:54 -03:00
cheloRydel
534adc7ad3 feat(openamt): rename packages 2021-11-19 09:57:56 -03:00
cheloRydel
31daea2801 feat(openamt): decode and validate certificate 2021-11-18 18:33:35 -03:00
cheloRydel
607e26d0e7 feat(openamt): save amt configuration as settings, improve UX 2021-11-18 12:23:57 -03:00
cheloRydel
8b26ab1185 fix(openamt): use correct templates URL 2021-11-18 12:23:57 -03:00
cheloRydel
0368f6f678 feat(openamt): properly read .pfx cert file 2021-11-18 12:23:57 -03:00
cheloRydel
1584999d50 feat(openamt): remove TODO 2021-11-18 12:23:57 -03:00
cheloRydel
832fb88ce2 feat(openamt): check for feature flag on the backend 2021-11-18 12:23:57 -03:00
cheloRydel
b9fed9a133 feat(openamt): prompt user for MPS login, add form controls 2021-11-18 12:23:57 -03:00
Sven Dowideit
afccfc1d83 add server side log output for all error conditions
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2021-11-18 12:23:57 -03:00
cheloRydel
d3b429227d feat(openamt): change wordings 2021-11-18 12:23:56 -03:00
cheloRydel
ebc3c2b2ed feat(openamt): create or update wireless config 2021-11-18 12:23:56 -03:00
cheloRydel
7801cabb01 feat(openamt): create or update domain 2021-11-18 12:23:56 -03:00
cheloRydel
27c0f6d3bb feat(openamt): create or update CIRA config and AMT profile 2021-11-18 12:23:56 -03:00
cheloRydel
020c2efa42 feat(openamt): initial setup and create default CIRA config 2021-11-18 12:23:56 -03:00
cheloRydel
5a5b8911e1 feat(openamt): add openamt config form in settings view 2021-11-18 12:23:56 -03:00
cheloRydel
4ab39538df add component 2021-11-18 12:23:56 -03:00
27 changed files with 1642 additions and 14 deletions

View File

@@ -75,7 +75,7 @@ The feature request process is similar to the bug report process but has an extr
![portainer_featurerequest_workflow](https://user-images.githubusercontent.com/5485061/45727229-5ad39f00-bbf5-11e8-9550-16ba66c50615.png)
## Build Portainer locally
## Build and run Portainer locally
Ensure you have Docker, Node.js, yarn, and Golang installed in the correct versions.
@@ -85,7 +85,7 @@ Install dependencies with yarn:
$ yarn
```
Then build and run the project:
Then build and run the project in a Docker container:
```sh
$ yarn start
@@ -95,6 +95,16 @@ Portainer can now be accessed at <https://localhost:9443>.
Find more detailed steps at <https://documentation.portainer.io/contributing/instructions/>.
### Build customisation
By default, `yarn start` will use `/tmp/portainer/` for the data store, so it won't persist over reboots.
You can customise the following settings:
- `PORTAINER_DATA`: The host dir or volume name used by portainer (default `/tmp/portainer`)
- `PORTAINER_PROJECT`: The root dir of the repository - `${portainerRoot}/dist/` is imported into the container to get the build artifacts and external tools (defaults to `your current dir`)
- `PORTAINER_FLAGS`: a list of flags to be used on the portainer commandline, in the form `--admin-password=<pwd hash> --feat fdo=false --feat open-amt` (default: `""`)
## Adding api docs
When adding a new resource (or a route handler), we should add a new tag to api/http/handler/handler.go#L136 like this:

View File

@@ -8,17 +8,17 @@ import (
"strconv"
"strings"
"github.com/portainer/libhelm"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt"
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/libhelm"
"github.com/portainer/portainer/api/exec"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/git"
"github.com/portainer/portainer/api/hostmanagement/openamt"
"github.com/portainer/portainer/api/http"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/http/proxy"
@@ -468,6 +468,8 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
gitService := initGitService()
openAMTService := openamt.NewService()
cryptoService := initCryptoService()
digitalSignatureService := initDigitalSignatureService()
@@ -623,6 +625,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
LDAPService: ldapService,
OAuthService: oauthService,
GitService: gitService,
OpenAMTService: openAMTService,
ProxyManager: proxyManager,
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
KubeConfigService: kubeConfigService,

View File

@@ -37,7 +37,7 @@ require (
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0
github.com/swaggo/swag v1.7.3
github.com/swaggo/swag v1.7.4
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
@@ -47,4 +47,5 @@ require (
k8s.io/api v0.22.2
k8s.io/apimachinery v0.22.2
k8s.io/client-go v0.22.2
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78
)

View File

@@ -691,8 +691,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/swaggo/swag v1.7.3 h1:ucB7irEdRrhjmW+Z1Ss4GjO68oPKQFjSgOR8BCAvcbU=
github.com/swaggo/swag v1.7.3/go.mod h1:zD8h6h4SPv7t3l+4BKdRquqW1ASWjKZgT6Qv9z3kNqI=
github.com/swaggo/swag v1.7.4 h1:up+ixy8yOqJKiFcuhMgkuYuF4xnevuhnFAXXF8OSfNg=
github.com/swaggo/swag v1.7.4/go.mod h1:zD8h6h4SPv7t3l+4BKdRquqW1ASWjKZgT6Qv9z3kNqI=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
@@ -790,7 +790,6 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1149,3 +1148,5 @@ sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZa
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 h1:SqYE5+A2qvRhErbsXFfUEUmpWEKxxRSMgGLkvRAFOV4=
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78/go.mod h1:B7Wf0Ya4DHF9Yw+qfZuJijQYkWicqDa+79Ytmmq3Kjg=

View File

@@ -0,0 +1,49 @@
package openamt
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
portainer "github.com/portainer/portainer/api"
)
type authenticationResponse struct {
Token string `json:"token"`
}
func (service *Service) executeAuthenticationRequest(configuration portainer.OpenAMTConfiguration) (*authenticationResponse, error) {
loginURL := fmt.Sprintf("https://%v/mps/login/api/v1/authorize", configuration.MPSURL)
payload := map[string]string{
"username": configuration.Credentials.MPSUser,
"password": configuration.Credentials.MPSPassword,
}
jsonValue, _ := json.Marshal(payload)
req, err := http.NewRequest(http.MethodPost, loginURL, bytes.NewBuffer(jsonValue))
req.Header.Set("Content-Type", "application/json")
response, err := service.httpsClient.Do(req)
if err != nil {
return nil, err
}
responseBody, readErr := ioutil.ReadAll(response.Body)
if readErr != nil {
return nil, readErr
}
errorResponse := parseError(responseBody)
if errorResponse != nil {
return nil, errorResponse
}
var token authenticationResponse
err = json.Unmarshal(responseBody, &token)
if err != nil {
return nil, err
}
return &token, nil
}

View File

@@ -0,0 +1,140 @@
package openamt
import (
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net"
"net/http"
"strings"
portainer "github.com/portainer/portainer/api"
)
type CIRAConfig struct {
ConfigName string `json:"configName"`
MPSServerAddress string `json:"mpsServerAddress"`
ServerAddressFormat int `json:"serverAddressFormat"`
CommonName string `json:"commonName"`
MPSPort int `json:"mpsPort"`
Username string `json:"username"`
MPSRootCertificate string `json:"mpsRootCertificate"`
RegeneratePassword bool `json:"regeneratePassword"`
AuthMethod int `json:"authMethod"`
}
func (service *Service) createOrUpdateCIRAConfig(configuration portainer.OpenAMTConfiguration, configName string) (*CIRAConfig, error) {
ciraConfig, err := service.getCIRAConfig(configuration, configName)
if err != nil {
return nil, err
}
method := http.MethodPost
if ciraConfig != nil {
method = http.MethodPatch
}
ciraConfig, err = service.saveCIRAConfig(method, configuration, configName)
if err != nil {
return nil, err
}
return ciraConfig, nil
}
func (service *Service) getCIRAConfig(configuration portainer.OpenAMTConfiguration, configName string) (*CIRAConfig, error) {
url := fmt.Sprintf("https://%v/rps/api/v1/admin/ciraconfigs/%v", configuration.MPSURL, configName)
responseBody, err := service.executeGetRequest(url, configuration.Credentials.MPSToken)
if err != nil {
return nil, err
}
if responseBody == nil {
return nil, nil
}
var result CIRAConfig
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}
func (service *Service) saveCIRAConfig(method string, configuration portainer.OpenAMTConfiguration, configName string) (*CIRAConfig, error) {
url := fmt.Sprintf("https://%v/rps/api/v1/admin/ciraconfigs", configuration.MPSURL)
certificate, err := service.getCIRACertificate(configuration)
if err != nil {
return nil, err
}
addressFormat, err := addressFormat(configuration.MPSURL)
if err != nil {
return nil, err
}
config := CIRAConfig{
ConfigName: configName,
MPSServerAddress: configuration.MPSURL,
CommonName: configuration.MPSURL,
ServerAddressFormat: addressFormat,
MPSPort: 4433,
Username: "admin",
MPSRootCertificate: certificate,
RegeneratePassword: false,
AuthMethod: 2,
}
payload, _ := json.Marshal(config)
responseBody, err := service.executeSaveRequest(method, url, configuration.Credentials.MPSToken, payload)
if err != nil {
return nil, err
}
var result CIRAConfig
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}
func addressFormat(url string) (int, error) {
ip := net.ParseIP(url)
if ip == nil {
return 201, nil // FQDN
}
if strings.Contains(url, ".") {
return 3, nil // IPV4
}
if strings.Contains(url, ":") {
return 4, nil // IPV6
}
return 0, fmt.Errorf("could not determine server address format for %v", url)
}
func (service *Service) getCIRACertificate(configuration portainer.OpenAMTConfiguration) (string, error) {
loginURL := fmt.Sprintf("https://%v/mps/api/v1/ciracert", configuration.MPSURL)
req, err := http.NewRequest(http.MethodGet, loginURL, nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", configuration.Credentials.MPSToken))
response, err := service.httpsClient.Do(req)
if err != nil {
return "", err
}
if response.StatusCode != http.StatusOK {
return "", errors.New(fmt.Sprintf("unexpected status code %v", response.Status))
}
certificate, err := io.ReadAll(response.Body)
if err != nil {
return "", err
}
block, _ := pem.Decode(certificate)
return base64.StdEncoding.EncodeToString(block.Bytes), nil
}

View File

@@ -0,0 +1,81 @@
package openamt
import (
"encoding/json"
"fmt"
"net/http"
portainer "github.com/portainer/portainer/api"
)
type (
Domain struct {
DomainName string `json:"profileName"`
DomainSuffix string `json:"domainSuffix"`
ProvisioningCert string `json:"provisioningCert"`
ProvisioningCertPassword string `json:"provisioningCertPassword"`
ProvisioningCertStorageFormat string `json:"provisioningCertStorageFormat"`
}
)
func (service *Service) createOrUpdateDomain(configuration portainer.OpenAMTConfiguration) (*Domain, error) {
domain, err := service.getDomain(configuration)
if err != nil {
return nil, err
}
method := http.MethodPost
if domain != nil {
method = http.MethodPatch
}
domain, err = service.saveDomain(method, configuration)
if err != nil {
return nil, err
}
return domain, nil
}
func (service *Service) getDomain(configuration portainer.OpenAMTConfiguration) (*Domain, error) {
url := fmt.Sprintf("https://%v/rps/api/v1/admin/domains/%v", configuration.MPSURL, configuration.DomainConfiguration.DomainName)
responseBody, err := service.executeGetRequest(url, configuration.Credentials.MPSToken)
if err != nil {
return nil, err
}
if responseBody == nil {
return nil, nil
}
var result Domain
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}
func (service *Service) saveDomain(method string, configuration portainer.OpenAMTConfiguration) (*Domain, error) {
url := fmt.Sprintf("https://%v/rps/api/v1/admin/domains", configuration.MPSURL)
profile := Domain{
DomainName: configuration.DomainConfiguration.DomainName,
DomainSuffix: configuration.DomainConfiguration.DomainName,
ProvisioningCert: configuration.DomainConfiguration.CertFileText,
ProvisioningCertPassword: configuration.DomainConfiguration.CertPassword,
ProvisioningCertStorageFormat: "string",
}
payload, _ := json.Marshal(profile)
responseBody, err := service.executeSaveRequest(method, url, configuration.Credentials.MPSToken, payload)
if err != nil {
return nil, err
}
var result Domain
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}

View File

@@ -0,0 +1,104 @@
package openamt
import (
"encoding/json"
"fmt"
"net/http"
portainer "github.com/portainer/portainer/api"
)
type (
Profile struct {
ProfileName string `json:"profileName"`
Activation string `json:"activation"`
CIRAConfigName *string `json:"ciraConfigName"`
GenerateRandomAMTPassword bool `json:"generateRandomPassword"`
AMTPassword string `json:"amtPassword"`
GenerateRandomMEBxPassword bool `json:"generateRandomMEBxPassword"`
MEBXPassword string `json:"mebxPassword"`
Tags []string `json:"tags"`
DHCPEnabled bool `json:"dhcpEnabled"`
TenantId string `json:"tenantId"`
WIFIConfigs []ProfileWifiConfig `json:"wifiConfigs"`
}
ProfileWifiConfig struct {
Priority int `json:"priority"`
ProfileName string `json:"profileName"`
}
)
func (service *Service) createOrUpdateAMTProfile(configuration portainer.OpenAMTConfiguration, profileName string, ciraConfigName string, wirelessConfig string) (*Profile, error) {
profile, err := service.getAMTProfile(configuration, profileName)
if err != nil {
return nil, err
}
method := http.MethodPost
if profile != nil {
method = http.MethodPatch
}
profile, err = service.saveAMTProfile(method, configuration, profileName, ciraConfigName, wirelessConfig)
if err != nil {
return nil, err
}
return profile, nil
}
func (service *Service) getAMTProfile(configuration portainer.OpenAMTConfiguration, profileName string) (*Profile, error) {
url := fmt.Sprintf("https://%v/rps/api/v1/admin/profiles/%v", configuration.MPSURL, profileName)
responseBody, err := service.executeGetRequest(url, configuration.Credentials.MPSToken)
if err != nil {
return nil, err
}
if responseBody == nil {
return nil, nil
}
var result Profile
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}
func (service *Service) saveAMTProfile(method string, configuration portainer.OpenAMTConfiguration, profileName string, ciraConfigName string, wirelessConfig string) (*Profile, error) {
url := fmt.Sprintf("https://%v/rps/api/v1/admin/profiles", configuration.MPSURL)
profile := Profile{
ProfileName: profileName,
Activation: "acmactivate",
GenerateRandomAMTPassword: false,
GenerateRandomMEBxPassword: false,
AMTPassword: configuration.Credentials.MPSPassword,
MEBXPassword: configuration.Credentials.MPSPassword,
CIRAConfigName: &ciraConfigName,
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)
if err != nil {
return nil, err
}
var result Profile
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}

View File

@@ -0,0 +1,91 @@
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://%v/rps/api/v1/admin/wirelessconfigs/%v", configuration.MPSURL, 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: %v", err.Error())
}
parsedEncryptionMethod, err := strconv.Atoi(configuration.WirelessConfiguration.EncryptionMethod)
if err != nil {
return nil, fmt.Errorf("error parsing wireless encryption method: %v", err.Error())
}
url := fmt.Sprintf("https://%v/rps/api/v1/admin/wirelessconfigs", configuration.MPSURL)
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,148 @@
package openamt
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"time"
portainer "github.com/portainer/portainer/api"
)
const (
DefaultCIRAConfigName = "ciraConfigDefault"
DefaultWirelessConfigName = "wirelessProfileDefault"
DefaultProfileName = "profileAMTDefault"
)
// Service represents a service for managing an OpenAMT server.
type Service struct {
httpsClient *http.Client
}
// NewService initializes a new service.
func NewService() *Service {
return &Service{
httpsClient:
&http.Client{
Timeout: time.Second * time.Duration(5),
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
},
}
}
type openAMTError struct {
ErrorMsg string `json:"message"`
Errors []struct {
ErrorMsg string `json:"msg"`
} `json:"errors"`
}
func parseError(responseBody []byte) error {
var errorResponse openAMTError
err := json.Unmarshal(responseBody, &errorResponse)
if err != nil {
return err
}
if len(errorResponse.Errors) > 0 {
return errors.New(errorResponse.Errors[0].ErrorMsg)
}
if errorResponse.ErrorMsg != "" {
return errors.New(errorResponse.ErrorMsg)
}
return nil
}
func (service *Service) ConfigureDefault(configuration portainer.OpenAMTConfiguration) error {
token, err := service.executeAuthenticationRequest(configuration)
if err != nil {
return err
}
configuration.Credentials.MPSToken = token.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)
if err != nil {
return err
}
_, err = service.createOrUpdateDomain(configuration)
if err != nil {
return err
}
return nil
}
func (service *Service) executeSaveRequest(method string, url string, token string, payload []byte) ([]byte, error) {
req, err := http.NewRequest(method, url, bytes.NewBuffer(payload))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token))
response, err := service.httpsClient.Do(req)
if err != nil {
return nil, err
}
responseBody, readErr := ioutil.ReadAll(response.Body)
if readErr != nil {
return nil, readErr
}
if response.StatusCode < 200 || response.StatusCode > 300 {
errorResponse := parseError(responseBody)
if errorResponse != nil {
return nil, errorResponse
}
return nil, errors.New(fmt.Sprintf("unexpected status code %v", response.Status))
}
return responseBody, nil
}
func (service *Service) executeGetRequest(url string, token string) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token))
response, err := service.httpsClient.Do(req)
if err != nil {
return nil, err
}
responseBody, readErr := ioutil.ReadAll(response.Body)
if readErr != nil {
return nil, readErr
}
if response.StatusCode < 200 || response.StatusCode > 300 {
if response.StatusCode == http.StatusNotFound {
return nil, nil
}
errorResponse := parseError(responseBody)
if errorResponse != nil {
return nil, errorResponse
}
return nil, errors.New(fmt.Sprintf("unexpected status code %v", response.Status))
}
return responseBody, 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/openamt"
"github.com/portainer/portainer/api/http/handler/kubernetes"
"github.com/portainer/portainer/api/http/handler/ldap"
"github.com/portainer/portainer/api/http/handler/motd"
@@ -62,6 +63,7 @@ type Handler struct {
RoleHandler *roles.Handler
SettingsHandler *settings.Handler
SSLHandler *ssl.Handler
OpenAMTHandler *openamt.Handler
StackHandler *stacks.Handler
StatusHandler *status.Handler
StorybookHandler *storybook.Handler
@@ -221,6 +223,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.UserHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/ssl"):
http.StripPrefix("/api", h.SSLHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/open_amt"):
http.StripPrefix("/api", h.OpenAMTHandler).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,197 @@
package openamt
import (
"context"
"io"
"io/ioutil"
"log"
"net/http"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"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/bolt/errors"
"github.com/sirupsen/logrus"
)
type OpenAMTHostInfo struct {
Endpoint portainer.EndpointID
Text string
}
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 = "ptrrd/openamt:rpc-go"
rpcGoContainerName = "openamt-rpc-go"
)
// @id OpenAMTHostInfo
// @summary Request OpenAMT info from a node
// @description Request OpenAMT info from a node
// @description **Access policy**: administrator
// @tags intel
// @security jwt
// @accept json
// @produce json
// @success 204 "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied to access settings"
// @failure 500 "Server error"
// @router /manage/{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}
}
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 assuption 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 &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: output, Err: err}
}
amtInfo := OpenAMTHostInfo{
Endpoint: portainer.EndpointID(endpointID),
Text: output,
}
return response.JSON(w, amtInfo)
}
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 ptrrd/openamt:rpc-go amtinfo
// on the Docker standalone node (one per env :)
// and later, on the specified node in the swarm, or kube.
nodeName := ""
docker, err := handler.DockerClientFactory.CreateClient(endpoint, nodeName)
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 programatically, and run it to get the result I'm getting here.
// TODO: likley an upgrade and absrtaction of DeployComposeStack/DeploySwarmStack/DeployKubernetesStack
// pullImage will pull the image to the specified environment
// TODO: add k8s implemenation
// TODO: work out registry auth
func pullImage(ctx context.Context, docker *client.Client, imageName string) error {
r, 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
}
// yeah, swiped this, need to figure out a good way to wait til its done...
b := make([]byte, 8)
for {
_, err := r.Read(b)
// TODO: should convert json text to a struct and show just the text messages
//if n > 0 {
//fmt.Printf(string(b))
//}
if err == io.EOF {
break
}
}
r.Close()
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) {
envs := []string{}
create, err := docker.ContainerCreate(
ctx,
&container.Config{
Image: imageName,
Cmd: cmdLine,
Env: envs,
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, create.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, create.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, create.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, create.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
}
return string(outputBytes), nil
}

View File

@@ -0,0 +1,42 @@
package openamt
import (
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/http/security"
)
// Handler is the HTTP handler used to handle OpenAMT operations.
type Handler struct {
*mux.Router
OpenAMTService portainer.OpenAMTService
DataStore portainer.DataStore
// used by OpenAMTHostInfo
DockerClientFactory *docker.ClientFactory
}
// NewHandler returns a new Handler
func NewHandler(bouncer *security.RequestBouncer, dataStore portainer.DataStore) (*Handler, error) {
h := &Handler{
Router: mux.NewRouter(),
}
settings, err := dataStore.Settings().Settings()
if err != nil {
return nil, err
}
featureEnabled, _ := settings.FeatureFlagSettings[portainer.FeatOpenAMT]
if featureEnabled {
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)
}
return h, nil
}

View File

@@ -0,0 +1,218 @@
package openamt
import (
"encoding/base64"
"errors"
"fmt"
"net/http"
"strings"
"github.com/sirupsen/logrus"
"software.sslmate.com/src/go-pkcs12"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
)
type openAMTConfigureDefaultPayload struct {
EnableOpenAMT bool
MPSURL string
MPSUser string
MPSPassword string
CertFileText string
CertPassword string
DomainName string
UseWirelessConfig bool
WifiAuthenticationMethod string
WifiEncryptionMethod string
WifiSSID string
WifiPskPass string
}
func (payload *openAMTConfigureDefaultPayload) Validate(r *http.Request) error {
if payload.EnableOpenAMT {
if payload.MPSURL == "" {
return errors.New("MPS Url must be provided")
}
if payload.MPSUser == "" {
return errors.New("MPS User must be provided")
}
if payload.MPSPassword == "" {
return errors.New("MPS Password must be provided")
}
if payload.DomainName == "" {
return errors.New("domain name must be provided")
}
if payload.CertFileText == "" {
return errors.New("certificate file must be provided")
}
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
}
// @id OpenAMTConfigureDefault
// @summary Enable OpenAMT capabilities
// @description Enable OpenAMT capabilities
// @description **Access policy**: administrator
// @tags intel
// @security jwt
// @accept json
// @produce json
// @param body body openAMTConfigureDefaultPayload true "OpenAMT Settings"
// @success 204 "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied to access settings"
// @failure 500 "Server error"
// @router /open_amt [post]
func (handler *Handler) openAMTConfigureDefault(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload openAMTConfigureDefaultPayload
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}
}
if payload.EnableOpenAMT {
certificateErr := validateCertificate(payload.CertFileText, payload.CertPassword)
if certificateErr != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Error validating certificate", Err: certificateErr}
}
err = handler.enableOpenAMT(payload)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Error enabling OpenAMT", Err: err}
}
return response.Empty(w)
}
err = handler.disableOpenAMT()
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Error disabling OpenAMT", Err: err}
}
return response.Empty(w)
}
func validateCertificate(certificateRaw string, certificatePassword string) error {
certificateData, err := base64.StdEncoding.Strict().DecodeString(certificateRaw)
if err != nil {
return err
}
_, certificate, _, err := pkcs12.DecodeChain(certificateData, certificatePassword)
if err != nil {
return err
}
if certificate == nil {
return errors.New("certificate could not be decoded")
}
issuer := certificate.Issuer.CommonName
if !isValidIssuer(issuer) {
return fmt.Errorf("certificate issuer is invalid: %v", issuer)
}
return nil
}
func isValidIssuer(issuer string) bool {
formattedIssuer := strings.ToLower(strings.ReplaceAll(issuer, " ", ""))
return strings.Contains(formattedIssuer, "comodo") ||
strings.Contains(formattedIssuer, "digicert") ||
strings.Contains(formattedIssuer, "entrust") ||
strings.Contains(formattedIssuer, "godaddy")
}
func (handler *Handler) enableOpenAMT(configurationPayload openAMTConfigureDefaultPayload) error {
configuration := portainer.OpenAMTConfiguration{
Enabled: true,
MPSURL: configurationPayload.MPSURL,
Credentials: portainer.MPSCredentials{
MPSUser: configurationPayload.MPSUser,
MPSPassword: configurationPayload.MPSPassword,
},
DomainConfiguration: portainer.DomainConfiguration{
CertFileText: configurationPayload.CertFileText,
CertPassword: configurationPayload.CertPassword,
DomainName: configurationPayload.DomainName,
},
}
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")
return err
}
err = handler.saveConfiguration(configuration)
if err != nil {
logrus.WithError(err).Error("error updating OpenAMT configurations")
return err
}
logrus.Info("OpenAMT successfully enabled")
return nil
}
func (handler *Handler) saveConfiguration(configuration portainer.OpenAMTConfiguration) error {
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return err
}
configuration.Credentials.MPSToken = ""
settings.OpenAMTConfiguration = configuration
err = handler.DataStore.Settings().UpdateSettings(settings)
if err != nil {
return err
}
return nil
}
func (handler *Handler) disableOpenAMT() error {
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return err
}
settings.OpenAMTConfiguration.Enabled = false
err = handler.DataStore.Settings().UpdateSettings(settings)
if err != nil {
return err
}
logrus.Info("OpenAMT successfully disabled")
return nil
}

View File

@@ -28,6 +28,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/openamt"
kubehandler "github.com/portainer/portainer/api/http/handler/kubernetes"
"github.com/portainer/portainer/api/http/handler/ldap"
"github.com/portainer/portainer/api/http/handler/motd"
@@ -75,6 +76,7 @@ type Server struct {
FileService portainer.FileService
DataStore portainer.DataStore
GitService portainer.GitService
OpenAMTService portainer.OpenAMTService
JWTService portainer.JWTService
LDAPService portainer.LDAPService
OAuthService portainer.OAuthService
@@ -203,6 +205,14 @@ func (server *Server) Start() error {
var sslHandler = sslhandler.NewHandler(requestBouncer)
sslHandler.SSLService = server.SSLService
openAMTHandler, err := openamt.NewHandler(requestBouncer, server.DataStore)
if err != nil {
return err
}
openAMTHandler.OpenAMTService = server.OpenAMTService
openAMTHandler.DataStore = server.DataStore
openAMTHandler.DockerClientFactory = server.DockerClientFactory
var stackHandler = stacks.NewHandler(requestBouncer)
stackHandler.DataStore = server.DataStore
stackHandler.DockerClientFactory = server.DockerClientFactory
@@ -268,6 +278,7 @@ func (server *Server) Start() error {
HelmTemplatesHandler: helmTemplatesHandler,
KubernetesHandler: kubernetesHandler,
MOTDHandler: motdHandler,
OpenAMTHandler: openAMTHandler,
RegistryHandler: registryHandler,
ResourceControlHandler: resourceControlHandler,
SettingsHandler: settingsHandler,

View File

@@ -40,6 +40,34 @@ type (
AuthenticationKey string `json:"AuthenticationKey" example:"cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk="`
}
// OpenAMTConfiguration represents the credentials and configurations used to connect to an OpenAMT MPS server
OpenAMTConfiguration struct {
Enabled bool `json:"Enabled"`
MPSURL string `json:"MPSURL"`
Credentials MPSCredentials `json:"Credentials"`
DomainConfiguration DomainConfiguration `json:"DomainConfiguration"`
WirelessConfiguration *WirelessConfiguration `json:"WirelessConfiguration"`
}
MPSCredentials struct {
MPSUser string `json:"MPSUser"`
MPSPassword string `json:"MPSPassword"`
MPSToken string `json:"MPSToken"` // retrieved from API
}
DomainConfiguration struct {
CertFileText string `json:"CertFileText"`
CertPassword string `json:"CertPassword"`
DomainName string `json:"DomainName"`
}
WirelessConfiguration struct {
AuthenticationMethod string `json:"AuthenticationMethod"`
EncryptionMethod string `json:"EncryptionMethod"`
SSID string `json:"SSID"`
PskPass string `json:"PskPass"`
}
// CLIFlags represents the available flags on the CLI
CLIFlags struct {
Addr *string
@@ -708,6 +736,7 @@ type (
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod" example:"1"`
LDAPSettings LDAPSettings `json:"LDAPSettings" example:""`
OAuthSettings OAuthSettings `json:"OAuthSettings" example:""`
OpenAMTConfiguration OpenAMTConfiguration `json:"OpenAMTConfiguration" example:""`
FeatureFlagSettings map[Feature]bool `json:"FeatureFlagSettings" example:""`
// The interval in which environment(endpoint) snapshots are created
SnapshotInterval string `json:"SnapshotInterval" example:"5m"`
@@ -1257,6 +1286,11 @@ type (
LatestCommitID(repositoryURL, referenceName, username, password string) (string, error)
}
// OpenAMTService represents a service for managing OpenAMT
OpenAMTService interface {
ConfigureDefault(configuration OpenAMTConfiguration) error
}
// HelmUserRepositoryService represents a service to manage HelmUserRepositories
HelmUserRepositoryService interface {
HelmUserRepositoryByUserID(userID UserID) ([]HelmUserRepository, error)

View File

@@ -4,6 +4,7 @@ export function SettingsViewModel(data) {
this.AuthenticationMethod = data.AuthenticationMethod;
this.LDAPSettings = data.LDAPSettings;
this.OAuthSettings = new OAuthSettingsViewModel(data.OAuthSettings);
this.OpenAMTConfiguration = data.OpenAMTConfiguration;
this.SnapshotInterval = data.SnapshotInterval;
this.TemplatesURL = data.TemplatesURL;
this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval;

View File

@@ -0,0 +1,17 @@
import angular from 'angular';
const API_ENDPOINT_OPEN_AMT = 'api/open_amt';
angular.module('portainer.app').factory('OpenAMT', OpenAMTFactory);
/* @ngInject */
function OpenAMTFactory($resource) {
return $resource(
API_ENDPOINT_OPEN_AMT + '/:id/:action',
{},
{
submit: { method: 'POST' },
info: { method: 'GET', params: { id: '@id', action: 'info' } },
}
);
}

View File

@@ -0,0 +1,19 @@
import angular from 'angular';
angular.module('portainer.app').service('OpenAMTService', OpenAMTServiceFactory);
/* @ngInject */
function OpenAMTServiceFactory(OpenAMT) {
return {
submit,
info,
};
function submit(formValues) {
return OpenAMT.submit(formValues).$promise;
}
function info(endpointID) {
return OpenAMT.info({ id: endpointID }).$promise;
}
}

View File

@@ -1,5 +1,6 @@
import angular from 'angular';
import { sslCertificate } from './ssl-certificate';
import { openAMT } from './open-amt';
export default angular.module('portainer.settings.general', []).component('sslCertificateSettings', sslCertificate).name;
export default angular.module('portainer.settings.general', []).component('sslCertificateSettings', sslCertificate).component('openAmtSettings', openAMT).name;

View File

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

View File

@@ -0,0 +1,108 @@
class OpenAmtController {
/* @ngInject */
constructor($async, $state, OpenAMTService, SettingsService, Notifications) {
Object.assign(this, { $async, $state, OpenAMTService, SettingsService, Notifications });
this.originalValues = {};
this.formValues = {
enableOpenAMT: false,
mpsURL: '',
mpsUser: '',
mpsPassword: '',
domainName: '',
certFile: null,
certPassword: '',
useWirelessConfig: false,
wifiAuthenticationMethod: '4',
wifiEncryptionMethod: '3',
wifiSsid: '',
wifiPskPass: '',
};
this.originalValues = {
...this.formValues,
};
this.state = {
actionInProgress: false,
};
this.save = this.save.bind(this);
}
isFormChanged() {
return Object.entries(this.originalValues).some(([key, value]) => value !== this.formValues[key]);
}
isFormValid() {
return !this.formValues.enableOpenAMT || this.formValues.certFile != null;
}
async readFile() {
return new Promise((resolve, reject) => {
const file = this.formValues.certFile;
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);
}
});
}
async save() {
return this.$async(async () => {
this.state.actionInProgress = true;
try {
this.formValues.certFileText = this.formValues.certFile ? await this.readFile(this.formValues.certFile) : null;
await this.OpenAMTService.submit(this.formValues);
await new Promise((resolve) => setTimeout(resolve, 2000));
this.Notifications.success(`OpenAMT successfully ${this.formValues.enableOpenAMT ? 'enabled' : 'disabled'}`);
} 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.OpenAMTConfiguration;
if (config) {
this.formValues = {
...this.formValues,
enableOpenAMT: config.Enabled,
mpsURL: config.MPSURL,
mpsUser: config.Credentials.MPSUser,
domainName: config.DomainConfiguration.DomainName,
useWirelessConfig: config.WirelessConfiguration.UseWirelessConfig,
wifiAuthenticationMethod: config.WirelessConfiguration.AuthenticationMethod,
wifiEncryptionMethod: config.WirelessConfiguration.EncryptionMethod,
wifiSsid: config.WirelessConfiguration.SSID,
};
this.originalValues = {
...this.formValues,
};
}
} catch (err) {
this.Notifications.error('Failure', err, 'Failed loading settings');
}
});
}
}
export default OpenAmtController;

View File

@@ -0,0 +1,259 @@
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-laptop" title-text="Intel OpenAMT"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal" name="openAMTForm">
<por-switch-field ng-model="$ctrl.formValues.enableOpenAMT" label="Enable edge OpenAMT"></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 an OpenAMT MPS API.
</p>
</span>
<div ng-show="$ctrl.formValues.enableOpenAMT">
<hr />
<div class="form-group">
<label for="mps_url" class="col-sm-3 control-label text-left">
MPS URL
</label>
<div class="col-sm-8">
<input
type="text"
class="form-control"
ng-model="$ctrl.formValues.mpsURL"
id="mps_url"
name="mps_url"
placeholder="Enter the MPS URL"
ng-required="$ctrl.formValues.enableOpenAMT"
/>
</div>
</div>
<div class="form-group col-md-12" ng-show="openAMTForm.mps_url.$invalid">
<div class="small text-warning">
<div ng-messages="openAMTForm.mps_url.$error">
<p ng-message="required"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<div class="form-group">
<label for="mps_user" class="col-sm-3 control-label text-left">
MPS User
</label>
<div class="col-sm-8">
<input
type="text"
class="form-control"
ng-model="$ctrl.formValues.mpsUser"
id="mps_user"
name="mps_user"
placeholder="Enter the MPS User"
ng-required="$ctrl.formValues.enableOpenAMT"
/>
</div>
</div>
<div class="form-group col-md-12" ng-show="openAMTForm.mps_user.$invalid">
<div class="small text-warning">
<div ng-messages="openAMTForm.mps_user.$error">
<p ng-message="required"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<div class="form-group">
<label for="mps_user" class="col-sm-3 control-label text-left">
MPS Password
<portainer-tooltip
position="bottom"
message="Needs to be 8-32 characters including one uppercase, one lowercase letters, one base-10 digit and one special character."
></portainer-tooltip>
</label>
<div class="col-sm-8">
<input
type="password"
class="form-control"
ng-model="$ctrl.formValues.mpsPassword"
id="mps_password"
name="mps_password"
placeholder="Enter the MPS Password"
ng-required="$ctrl.formValues.enableOpenAMT"
/>
</div>
</div>
<div class="form-group col-md-12" ng-show="openAMTForm.mps_password.$invalid">
<div class="small text-warning">
<div ng-messages="openAMTForm.mps_password.$error">
<p ng-message="required"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<hr />
<div class="form-group">
<label for="domain_name" class="col-sm-3 control-label text-left">
Domain Name
<portainer-tooltip position="bottom" message="Enter the FQDN that is associated with the provisioning certificate (i.e amtdomain.com)"></portainer-tooltip>
</label>
<div class="col-sm-8">
<input
type="text"
class="form-control"
ng-model="$ctrl.formValues.domainName"
id="domain_name"
name="domain_name"
placeholder="Enter the Domain Name"
ng-required="$ctrl.formValues.enableOpenAMT"
/>
</div>
</div>
<div class="form-group col-md-12" ng-show="openAMTForm.domain_name.$invalid">
<div class="small text-warning">
<div ng-messages="openAMTForm.domain_name.$error">
<p ng-message="required"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<div class="form-group">
<label for="certificate_file" class="col-sm-3 control-label text-left">
Provisioning Certificate File (.pfx)
<portainer-tooltip
position="bottom"
message="Supported CAs are Comodo, DigiCert, Entrust and GoDaddy. The certificate must contain the private key."
></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">
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>
</span>
</div>
</div>
<div class="form-group col-md-12" ng-show="openAMTForm.certFile.$invalid">
<div class="small text-warning">
<div ng-messages="openAMTForm.certFile.$error">
<p ng-message="pattern"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> File type is invalid.</p>
</div>
</div>
</div>
<div class="form-group">
<label for="certificate_password" class="col-sm-3 control-label text-left">
Provisioning Certificate Password
<portainer-tooltip
position="bottom"
message="Needs to be 8-32 characters including one uppercase, one lowercase letters, one base-10 digit and one special character."
></portainer-tooltip>
</label>
<div class="col-sm-8">
<input
type="password"
class="form-control"
ng-model="$ctrl.formValues.certPassword"
id="certificate_password"
name="certificate_password"
placeholder="**********"
ng-required="$ctrl.formValues.enableOpenAMT"
/>
</div>
</div>
<div class="form-group col-md-12" ng-show="openAMTForm.certificate_password.$invalid">
<div class="small text-warning">
<div ng-messages="openAMTForm.certificate_password.$error">
<p ng-message="required"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<hr />
<div class="form-group">
<div class="col-sm-12">
<por-switch-field ng-model="$ctrl.formValues.useWirelessConfig" label="Wireless Configuration"></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 />
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="$ctrl.state.actionInProgress || !openAMTForm.$valid || !$ctrl.isFormChanged() || !$ctrl.isFormValid()"
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

@@ -57,7 +57,8 @@
label="Allow self-signed certs"
ng-model="state.allowSelfSignedCerts"
tooltip="When allowing self-signed certificates the edge agent will ignore the domain validation when connecting to Portainer via HTTPS"
></por-switch-field>
>
</por-switch-field>
<div style="margin-top: 10px;">
<uib-tabset active="state.deploymentTab">
<uib-tab index="'kubernetes'" heading="Kubernetes" ng-if="state.platformType === 'linux'">
@@ -167,7 +168,8 @@
<portainer-tooltip
position="bottom"
message="Interval used by this Edge agent to check in with the Portainer instance. Affects Edge environment management and Edge compute features."
></portainer-tooltip>
>
</portainer-tooltip>
</label>
<div class="col-sm-10">
<select
@@ -210,6 +212,63 @@
</div>
<por-endpoint-security form-data="formValues.SecurityFormData" endpoint="endpoint"></por-endpoint-security>
</div>
<!-- open-amt info (settings.FeatureFlagSettings && settings.FeatureFlagSettings['open-amt']) -->
<div ng-if="state.managementinfo !== ''">
<div class="col-sm-12 form-section-title">
Open Active Management Technology
</div>
<div class="form-group">
<label for="endpoint_managementinfo" class="col-sm-3 col-lg-2 control-label text-left">
Debug Info
<portainer-tooltip position="bottom" message="AMT Debug.."> </portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
ng-disabled="true"
class="form-control"
id="endpoint_managementinfo"
ng-model="endpoint.ManagementInfoRaw"
placeholder="querying node AMT info..."
/>
</div>
</div>
<div class="form-group">
<label for="endpoint_managementinfoVersion" class="col-sm-3 col-lg-2 control-label text-left">
AMT Version
<portainer-tooltip position="bottom" message="AMT Version"> </portainer-tooltip>
</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="querying node AMT info..."
/>
</div>
</div>
<div class="form-group">
<label for="endpoint_managementinfoControlMode" class="col-sm-3 col-lg-2 control-label text-left">
Control Mode
<portainer-tooltip position="bottom" message="AMT Control Mode"> </portainer-tooltip>
</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="querying node AMT info..."
/>
</div>
</div>
</div>
<!-- !open-amt info -->
<!-- !endpoint-security -->
<div class="form-group">
<div class="col-sm-12">

View File

@@ -24,7 +24,8 @@ function EndpointController(
Authentication,
SettingsService,
ModalService,
StateManager
StateManager,
OpenAMTService
) {
const DEPLOYMENT_TABS = {
SWARM: 'swarm',
@@ -246,6 +247,7 @@ function EndpointController(
async function initView() {
return $async(async () => {
var openAmt = false;
try {
const [endpoint, groups, tags, settings] = await Promise.all([
EndpointService.endpoint($transition$.params().id),
@@ -265,7 +267,6 @@ function EndpointController(
if (endpoint.Type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment || endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) {
$scope.edgeKeyDetails = decodeEdgeKey(endpoint.EdgeKey);
$scope.randomEdgeID = uuidv4();
$scope.state.availableEdgeAgentCheckinOptions[0].key += ` (${settings.EdgeAgentCheckinInterval} seconds)`;
}
@@ -273,10 +274,30 @@ function EndpointController(
$scope.groups = groups;
$scope.availableTags = tags;
if (settings.FeatureFlagSettings && settings.FeatureFlagSettings['open-amt']) {
openAmt = true;
}
configureState();
} catch (err) {
Notifications.error('Failure', err, 'Unable to retrieve environment details');
}
if (openAmt) {
// TODO: if we know this env/node isn't connected (ie, we have no conectionto Docker), then don't try (is is a server side response?)
try {
const [amtinfo] = await Promise.all([OpenAMTService.info($transition$.params().id)]);
$scope.endpoint.ManagementInfoRaw = amtinfo.Text;
try {
$scope.endpoint.ManagementInfo = JSON.parse(amtinfo.Text);
} catch (err) {
console.log('Failure', err, 'Unable to JSON parse AMT info: ' + amtinfo.Text);
}
} catch (err) {
Notifications.error('Failure', err, 'Unable to retrieve AMT environment details');
}
}
});
}

View File

@@ -182,6 +182,8 @@
<ssl-certificate-settings></ssl-certificate-settings>
<open-amt-settings ng-show="settings.FeatureFlagSettings && settings.FeatureFlagSettings['open-amt']"></open-amt-settings>
<div class="row">
<div class="col-sm-12">
<rd-widget>

View File

@@ -158,6 +158,7 @@ function shell_build_binary_azuredevops(platform, arch) {
function shell_run_container() {
const portainerData = '${PORTAINER_DATA:-/tmp/portainer}';
const portainerRoot = process.env.PORTAINER_PROJECT ? process.env.PORTAINER_PROJECT : process.env.PWD;
const portainerFlags = '${PORTAINER_FLAGS:-}';
return `
docker rm -f portainer
@@ -172,7 +173,7 @@ function shell_run_container() {
-v /tmp:/tmp \
--name portainer \
portainer/base \
/app/portainer
/app/portainer ${portainerFlags}
`;
}