Compare commits
25 Commits
2.11.1
...
feat/INT-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d3c1396e7 | ||
|
|
d13f3e7548 | ||
|
|
4e006e21de | ||
|
|
81f3527556 | ||
|
|
7577a13589 | ||
|
|
21f6292582 | ||
|
|
bfcd6a3592 | ||
|
|
6a0f3ac618 | ||
|
|
ecab23e168 | ||
|
|
534adc7ad3 | ||
|
|
31daea2801 | ||
|
|
607e26d0e7 | ||
|
|
8b26ab1185 | ||
|
|
0368f6f678 | ||
|
|
1584999d50 | ||
|
|
832fb88ce2 | ||
|
|
b9fed9a133 | ||
|
|
afccfc1d83 | ||
|
|
d3b429227d | ||
|
|
ebc3c2b2ed | ||
|
|
7801cabb01 | ||
|
|
27c0f6d3bb | ||
|
|
020c2efa42 | ||
|
|
5a5b8911e1 | ||
|
|
4ab39538df |
@@ -75,7 +75,7 @@ The feature request process is similar to the bug report process but has an extr
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
49
api/hostmanagement/openamt/authorization.go
Normal file
49
api/hostmanagement/openamt/authorization.go
Normal 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
|
||||
}
|
||||
140
api/hostmanagement/openamt/configCIRA.go
Normal file
140
api/hostmanagement/openamt/configCIRA.go
Normal 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
|
||||
}
|
||||
81
api/hostmanagement/openamt/configDomain.go
Normal file
81
api/hostmanagement/openamt/configDomain.go
Normal 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
|
||||
}
|
||||
104
api/hostmanagement/openamt/configProfile.go
Normal file
104
api/hostmanagement/openamt/configProfile.go
Normal 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
|
||||
}
|
||||
91
api/hostmanagement/openamt/configWireless.go
Normal file
91
api/hostmanagement/openamt/configWireless.go
Normal 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
|
||||
}
|
||||
148
api/hostmanagement/openamt/openamt.go
Normal file
148
api/hostmanagement/openamt/openamt.go
Normal 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
|
||||
}
|
||||
@@ -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"):
|
||||
|
||||
197
api/http/handler/hostmanagement/openamt/amtrpc.go
Normal file
197
api/http/handler/hostmanagement/openamt/amtrpc.go
Normal 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
|
||||
}
|
||||
42
api/http/handler/hostmanagement/openamt/handler.go
Normal file
42
api/http/handler/hostmanagement/openamt/handler.go
Normal 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
|
||||
}
|
||||
218
api/http/handler/hostmanagement/openamt/openamt.go
Normal file
218
api/http/handler/hostmanagement/openamt/openamt.go
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
17
app/portainer/rest/openAMT.js
Normal file
17
app/portainer/rest/openAMT.js
Normal 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' } },
|
||||
}
|
||||
);
|
||||
}
|
||||
19
app/portainer/services/api/openAMTService.js
Normal file
19
app/portainer/services/api/openAMTService.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
6
app/portainer/settings/general/open-amt/index.js
Normal file
6
app/portainer/settings/general/open-amt/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import controller from './open-amt.controller.js';
|
||||
|
||||
export const openAMT = {
|
||||
templateUrl: './open-amt.html',
|
||||
controller,
|
||||
};
|
||||
108
app/portainer/settings/general/open-amt/open-amt.controller.js
Normal file
108
app/portainer/settings/general/open-amt/open-amt.controller.js
Normal 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;
|
||||
259
app/portainer/settings/general/open-amt/open-amt.html
Normal file
259
app/portainer/settings/general/open-amt/open-amt.html
Normal 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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user