mirror of
https://github.com/portainer/portainer.git
synced 2025-08-04 21:35:23 +02:00
feat(intel): Enable OpenAMT and FDO capabilities (#6212)
* feat(openamt): add AMT Devices information in Environments view [INT-8] (#6169) * feat(openamt): add AMT Devices Ouf of Band Managamenet actions [INT-9] (#6171) * feat(openamt): add AMT Devices KVM Connection [INT-10] (#6179) * feat(openamt): Enhance the Environments MX to activate OpenAMT on compatible environments [INT-7] (#6196) * feat(openamt): Enable KVM by default [INT-25] (#6228) * feat(fdo): implement the FDO configuration settings INT-19 (#6238) feat(fdo): implement the FDO configuration settings INT-19 * feat(fdo): implement Owner client INT-17 (#6231) feat(fdo): implement Owner client INT-17 * feat(openamt): hide wireless config in OpenAMT form (#6250) * feat(openamt): Increase OpenAMT timeouts [INT-30] (#6253) * feat(openamt): Disable the ability to use KVM and OOB actions on a MPS disconnected device [INT-36] (#6254) * feat(fdo): add import device UI [INT-20] (#6240) feat(fdo): add import device UI INT-20 * refactor(fdo): fix develop merge issues * feat(openamt): Do not fetch OpenAMT details for an unassociated Edge endpoint (#6273) * fix(intel): Fix switches params (#6282) * feat(openamt): preload existing AMT settings (#6283) * feat(openamt): Better UI/UX for AMT activation loading [INT-39] (#6290) * feat(openamt): Remove wireless config related code [INT-41] (#6291) * yarn install * feat(openamt): change kvm redirection for pop up, always enable features [INT-37] (#6292) * feat(openamt): change kvm redirection for pop up, always enable features [INT-37] (#6293) * feat(openmt): use .ts services with axios for OpenAMT (#6312) * Minor code cleanup. * fix(fdo): move the FDO client code to the hostmanagement folder INT-44 (#6345) * refactor(intel): Add Edge Compute Settings view (#6351) * feat(fdo): add FDO profiles INT-22 (#6363) feat(fdo): add FDO profiles INT-22 * fix(fdo): fix incorrect profile URL INT-45 (#6377) * fixed husky version * fix go.mod with go mod tidy * feat(edge): migrate OpenAMT devices views to Edge Devices [EE-2322] (#6373) * feat(intel): OpenAMT UI/UX adjustments (#6394) * only allow edge agent as edge device * show all edge agent environments on Edge Devices view * feat(fdo): add the ability to import multiple ownership vouchers at once EE-2324 (#6395) * fix(edge): settings edge compute alert (#6402) * remove pagination, add useMemo for devices result array (#6409) * feat(edge): minor Edge Devices (AMT) UI fixes (#6410) * chore(eslint): fix versions * chore(app): reformat codebase * change add edge agent modal behaviour, fix yarn.lock * fix use pagination * remove extractedTranslations folder * feat(edge): add FDO Profiles Datatable [EE-2406] (#6415) * feat(edge): add KVM workaround tooltip (#6441) * feat(edge): Add default FDO profile (#6450) * feat(edge): add settings to disable trust on first connect and enforce Edge ID INT-1 EE-2410 (#6429) Co-authored-by: andres-portainer <91705312+andres-portainer@users.noreply.github.com> Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io> Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com> Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
This commit is contained in:
parent
3ed92e5fee
commit
2c4c638f46
170 changed files with 6834 additions and 819 deletions
188
api/http/handler/hostmanagement/fdo/configure.go
Normal file
188
api/http/handler/hostmanagement/fdo/configure.go
Normal file
|
@ -0,0 +1,188 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
deploymentScriptName = "fdo.sh"
|
||||
)
|
||||
|
||||
type deviceConfigurePayload struct {
|
||||
EdgeID string `json:"edgeID"`
|
||||
EdgeKey string `json:"edgeKey"`
|
||||
Name string `json:"name"`
|
||||
ProfileID int `json:"profile"`
|
||||
}
|
||||
|
||||
func (payload *deviceConfigurePayload) Validate(r *http.Request) error {
|
||||
if payload.EdgeID == "" {
|
||||
return errors.New("invalid edge ID provided")
|
||||
}
|
||||
|
||||
if payload.EdgeKey == "" {
|
||||
return errors.New("invalid edge key provided")
|
||||
}
|
||||
|
||||
if payload.Name == "" {
|
||||
return errors.New("the device name cannot be empty")
|
||||
}
|
||||
|
||||
if payload.ProfileID < 1 {
|
||||
return errors.New("invalid profile id provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id fdoConfigureDevice
|
||||
// @summary configures an FDO device
|
||||
// @description configures an FDO device
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param body body deviceConfigurePayload true "Device Configuration"
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo/configure/{guid} [post]
|
||||
func (handler *Handler) fdoConfigureDevice(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
guid, err := request.RetrieveRouteVariableValue(r, "guid")
|
||||
if err != nil {
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: request.RetrieveRouteVariableValue()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: guid not found", Err: err}
|
||||
}
|
||||
|
||||
var payload deviceConfigurePayload
|
||||
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Invalid request payload")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
|
||||
profile, err := handler.DataStore.FDOProfile().FDOProfile(portainer.FDOProfileID(payload.ProfileID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a FDO Profile with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a FDO Profile with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
fileContent, err := handler.FileService.GetFileContent(profile.FilePath, "")
|
||||
if err != nil {
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: GetFileContent")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: GetFileContent", Err: err}
|
||||
}
|
||||
|
||||
fdoClient, err := handler.newFDOClient()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: newFDOClient()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: newFDOClient()", Err: err}
|
||||
}
|
||||
|
||||
// enable fdo_sys
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"0"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"active"},
|
||||
"bytes": []string{"F5"}, // this is "true" in CBOR
|
||||
}, []byte("")); err != nil {
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw()", Err: err}
|
||||
}
|
||||
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"1"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"filedesc"},
|
||||
"filename": []string{"DEVICE_edgeid.txt"},
|
||||
}, []byte(payload.EdgeID)); err != nil {
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw(edgeid)")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw(edgeid)", Err: err}
|
||||
}
|
||||
|
||||
// write down the edgekey
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"1"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"filedesc"},
|
||||
"filename": []string{"DEVICE_edgekey.txt"},
|
||||
}, []byte(payload.EdgeKey)); err != nil {
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw(edgekey)")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw(edgekey)", Err: err}
|
||||
}
|
||||
|
||||
// write down the device name
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"1"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"filedesc"},
|
||||
"filename": []string{"DEVICE_name.txt"},
|
||||
}, []byte(payload.Name)); err != nil {
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw(name)")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw(name)", Err: err}
|
||||
}
|
||||
|
||||
// write down the device GUID - used as the EDGE_DEVICE_GUID too
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"1"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"filedesc"},
|
||||
"filename": []string{"DEVICE_GUID.txt"},
|
||||
}, []byte(guid)); err != nil {
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw()", Err: err}
|
||||
}
|
||||
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"1"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"filedesc"},
|
||||
"filename": []string{deploymentScriptName},
|
||||
}, fileContent); err != nil {
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw()", Err: err}
|
||||
}
|
||||
|
||||
b, err := cbor.Marshal([]string{"/bin/sh", deploymentScriptName})
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("failed to marshal string to CBOR")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw() failed to encode", Err: err}
|
||||
}
|
||||
|
||||
cborBytes := strings.ToUpper(hex.EncodeToString(b))
|
||||
logrus.WithField("cbor", cborBytes).WithField("string", deploymentScriptName).Info("converted to CBOR")
|
||||
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"2"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"exec"},
|
||||
"bytes": []string{cborBytes},
|
||||
}, []byte("")); err != nil {
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw()", Err: err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
164
api/http/handler/hostmanagement/fdo/fdo.go
Normal file
164
api/http/handler/hostmanagement/fdo/fdo.go
Normal file
|
@ -0,0 +1,164 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/hostmanagement/fdo"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type fdoConfigurePayload portainer.FDOConfiguration
|
||||
|
||||
func validateURL(u string) error {
|
||||
p, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.Scheme != "http" && p.Scheme != "https" {
|
||||
return errors.New("invalid scheme provided, must be 'http' or 'https'")
|
||||
}
|
||||
|
||||
if p.Host == "" {
|
||||
return errors.New("invalid host provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (payload *fdoConfigurePayload) Validate(r *http.Request) error {
|
||||
if payload.Enabled {
|
||||
if err := validateURL(payload.OwnerURL); err != nil {
|
||||
return fmt.Errorf("owner server URL: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) saveSettings(config portainer.FDOConfiguration) error {
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.FDOConfiguration = config
|
||||
|
||||
return handler.DataStore.Settings().UpdateSettings(settings)
|
||||
}
|
||||
|
||||
func (handler *Handler) newFDOClient() (fdo.FDOOwnerClient, error) {
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return fdo.FDOOwnerClient{}, err
|
||||
}
|
||||
|
||||
return fdo.FDOOwnerClient{
|
||||
OwnerURL: settings.FDOConfiguration.OwnerURL,
|
||||
Username: settings.FDOConfiguration.OwnerUsername,
|
||||
Password: settings.FDOConfiguration.OwnerPassword,
|
||||
Timeout: 5 * time.Second,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// @id fdoConfigure
|
||||
// @summary Enable Portainer's FDO capabilities
|
||||
// @description Enable Portainer's FDO capabilities
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param body body fdoConfigurePayload true "FDO Settings"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo [post]
|
||||
func (handler *Handler) fdoConfigure(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload fdoConfigurePayload
|
||||
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Invalid request payload")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
|
||||
settings := portainer.FDOConfiguration(payload)
|
||||
if err = handler.saveSettings(settings); err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Error saving FDO settings", Err: err}
|
||||
}
|
||||
|
||||
profiles, err := handler.DataStore.FDOProfile().FDOProfiles()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Error saving FDO settings", Err: err}
|
||||
}
|
||||
if len(profiles) == 0 {
|
||||
err = handler.addDefaultProfile()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
func (handler *Handler) addDefaultProfile() error {
|
||||
profileID := handler.DataStore.FDOProfile().GetNextIdentifier()
|
||||
profile := &portainer.FDOProfile{
|
||||
ID: portainer.FDOProfileID(profileID),
|
||||
Name: "Docker Standalone + Edge",
|
||||
}
|
||||
|
||||
filePath, err := handler.FileService.StoreFDOProfileFileFromBytes(strconv.Itoa(int(profile.ID)), []byte(defaultProfileFileContent))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
profile.FilePath = filePath
|
||||
profile.DateCreated = time.Now().Unix()
|
||||
|
||||
err = handler.DataStore.FDOProfile().Create(profile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const defaultProfileFileContent = `
|
||||
#!/bin/bash -ex
|
||||
|
||||
env > env.log
|
||||
|
||||
export AGENT_IMAGE=portainer/agent:2.11.0
|
||||
export GUID=$(cat DEVICE_GUID.txt)
|
||||
export DEVICE_NAME=$(cat DEVICE_name.txt)
|
||||
export EDGE_ID=$(cat DEVICE_edgeid.txt)
|
||||
export EDGE_KEY=$(cat DEVICE_edgekey.txt)
|
||||
export AGENTVOLUME=$(pwd)/data/portainer_agent_data/
|
||||
|
||||
mkdir -p ${AGENTVOLUME}
|
||||
|
||||
docker pull ${AGENT_IMAGE}
|
||||
|
||||
docker run -d \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v /var/lib/docker/volumes:/var/lib/docker/volumes \
|
||||
-v /:/host \
|
||||
-v ${AGENTVOLUME}:/data \
|
||||
--restart always \
|
||||
-e EDGE=1 \
|
||||
-e EDGE_ID=${EDGE_ID} \
|
||||
-e EDGE_KEY=${EDGE_KEY} \
|
||||
-e CAP_HOST_MANAGEMENT=1 \
|
||||
-e EDGE_INSECURE_POLL=1 \
|
||||
--name portainer_edge_agent \
|
||||
${AGENT_IMAGE}
|
||||
`
|
40
api/http/handler/hostmanagement/fdo/handler.go
Normal file
40
api/http/handler/hostmanagement/fdo/handler.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore dataservices.DataStore
|
||||
FileService portainer.FileService
|
||||
}
|
||||
|
||||
func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore, fileService portainer.FileService) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
DataStore: dataStore,
|
||||
FileService: fileService,
|
||||
}
|
||||
|
||||
h.Handle("/fdo/configure", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoConfigure))).Methods(http.MethodPost)
|
||||
h.Handle("/fdo/list", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoListAll))).Methods(http.MethodGet)
|
||||
h.Handle("/fdo/register", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoRegisterDevice))).Methods(http.MethodPost)
|
||||
h.Handle("/fdo/configure/{guid}", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoConfigureDevice))).Methods(http.MethodPost)
|
||||
|
||||
h.Handle("/fdo/profiles", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoProfileList))).Methods(http.MethodGet)
|
||||
h.Handle("/fdo/profiles", bouncer.AdminAccess(httperror.LoggerHandler(h.createProfile))).Methods(http.MethodPost)
|
||||
h.Handle("/fdo/profiles/{id}", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoProfileInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/fdo/profiles/{id}", bouncer.AdminAccess(httperror.LoggerHandler(h.updateProfile))).Methods(http.MethodPut)
|
||||
h.Handle("/fdo/profiles/{id}", bouncer.AdminAccess(httperror.LoggerHandler(h.deleteProfile))).Methods(http.MethodDelete)
|
||||
h.Handle("/fdo/profiles/{id}/duplicate", bouncer.AdminAccess(httperror.LoggerHandler(h.duplicateProfile))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
39
api/http/handler/hostmanagement/fdo/list.go
Normal file
39
api/http/handler/hostmanagement/fdo/list.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
// @id fdoListAll
|
||||
// @summary List all known FDO vouchers
|
||||
// @description List all known FDO vouchers
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo/list [get]
|
||||
func (handler *Handler) fdoListAll(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
fdoClient, err := handler.newFDOClient()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Info("fdoListAll: newFDOClient()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoRegisterDevice: newFDOClient()", Err: err}
|
||||
}
|
||||
|
||||
// Get all vouchers
|
||||
guids, err := fdoClient.GetVouchers()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Info("fdoListAll: GetVouchers()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoListAll: GetVouchers()", Err: err}
|
||||
}
|
||||
|
||||
return response.JSON(w, guids)
|
||||
}
|
92
api/http/handler/hostmanagement/fdo/profile_create.go
Normal file
92
api/http/handler/hostmanagement/fdo/profile_create.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type createProfileFromFileContentPayload struct {
|
||||
Name string
|
||||
ProfileFileContent string
|
||||
}
|
||||
|
||||
func (payload *createProfileFromFileContentPayload) Validate(r *http.Request) error {
|
||||
if payload.Name == "" {
|
||||
return errors.New("profile name must be provided")
|
||||
}
|
||||
|
||||
if payload.ProfileFileContent == "" {
|
||||
return errors.New("profile file content must be provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id createProfile
|
||||
// @summary creates a new FDO Profile
|
||||
// @description creates a new FDO Profile
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 409 "Profile name already exists"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo/profiles [post]
|
||||
func (handler *Handler) createProfile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
method, err := request.RetrieveQueryParameter(r, "method", false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: method", err}
|
||||
}
|
||||
|
||||
switch method {
|
||||
case "editor":
|
||||
return handler.createFDOProfileFromFileContent(w, r)
|
||||
}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid method. Value must be one of: editor", errors.New("invalid method")}
|
||||
}
|
||||
|
||||
func (handler *Handler) createFDOProfileFromFileContent(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload createProfileFromFileContentPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
isUnique, err := handler.checkUniqueProfileName(payload.Name, -1)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
if !isUnique {
|
||||
return &httperror.HandlerError{http.StatusConflict, fmt.Sprintf("A profile with the name '%s' already exists", payload.Name), errors.New("a profile already exists with this name")}
|
||||
}
|
||||
|
||||
profileID := handler.DataStore.FDOProfile().GetNextIdentifier()
|
||||
profile := &portainer.FDOProfile{
|
||||
ID: portainer.FDOProfileID(profileID),
|
||||
Name: payload.Name,
|
||||
}
|
||||
|
||||
filePath, err := handler.FileService.StoreFDOProfileFileFromBytes(strconv.Itoa(int(profile.ID)), []byte(payload.ProfileFileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist profile file on disk", err}
|
||||
}
|
||||
profile.FilePath = filePath
|
||||
profile.DateCreated = time.Now().Unix()
|
||||
|
||||
err = handler.DataStore.FDOProfile().Create(profile)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the profile inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, profile)
|
||||
}
|
36
api/http/handler/hostmanagement/fdo/profile_delete.go
Normal file
36
api/http/handler/hostmanagement/fdo/profile_delete.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// @id deleteProfile
|
||||
// @summary deletes a FDO Profile
|
||||
// @description deletes a FDO Profile
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo/profiles/{id} [delete]
|
||||
func (handler *Handler) deleteProfile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
id, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Bad request", errors.New("missing 'id' query parameter")}
|
||||
}
|
||||
|
||||
err = handler.DataStore.FDOProfile().Delete(portainer.FDOProfileID(id))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete Profile", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
67
api/http/handler/hostmanagement/fdo/profile_duplicate.go
Normal file
67
api/http/handler/hostmanagement/fdo/profile_duplicate.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// @id duplicate
|
||||
// @summary duplicated an existing FDO Profile
|
||||
// @description duplicated an existing FDO Profile
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo/profiles/{id}/duplicate [post]
|
||||
func (handler *Handler) duplicateProfile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
id, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Bad request", errors.New("missing 'id' query parameter")}
|
||||
}
|
||||
|
||||
originalProfile, err := handler.DataStore.FDOProfile().FDOProfile(portainer.FDOProfileID(id))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a FDO Profile with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a FDO Profile with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
fileContent, err := handler.FileService.GetFileContent(originalProfile.FilePath, "")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Profile file content", err}
|
||||
}
|
||||
|
||||
profileID := handler.DataStore.FDOProfile().GetNextIdentifier()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to duplicate Profile", err}
|
||||
}
|
||||
|
||||
newProfile := &portainer.FDOProfile{
|
||||
ID: portainer.FDOProfileID(profileID),
|
||||
Name: fmt.Sprintf("%s - copy", originalProfile.Name),
|
||||
}
|
||||
|
||||
filePath, err := handler.FileService.StoreFDOProfileFileFromBytes(strconv.Itoa(int(newProfile.ID)), fileContent)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist profile file on disk", err}
|
||||
}
|
||||
newProfile.FilePath = filePath
|
||||
newProfile.DateCreated = time.Now().Unix()
|
||||
|
||||
err = handler.DataStore.FDOProfile().Create(newProfile)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the profile inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, newProfile)
|
||||
}
|
49
api/http/handler/hostmanagement/fdo/profile_inspect.go
Normal file
49
api/http/handler/hostmanagement/fdo/profile_inspect.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type fdoProfileResponse struct {
|
||||
Name string `json:"name"`
|
||||
FileContent string `json:"fileContent"`
|
||||
}
|
||||
|
||||
// @id fdoProfileInspect
|
||||
// @summary retrieves a given FDO profile information and content
|
||||
// @description retrieves a given FDO profile information and content
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo/profiles/{id} [get]
|
||||
func (handler *Handler) fdoProfileInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
id, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Bad request", errors.New("missing 'id' query parameter")}
|
||||
}
|
||||
|
||||
profile, err := handler.DataStore.FDOProfile().FDOProfile(portainer.FDOProfileID(id))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Profile", err}
|
||||
}
|
||||
|
||||
fileContent, err := handler.FileService.GetFileContent(profile.FilePath, "")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Profile file content", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, fdoProfileResponse{
|
||||
Name: profile.Name,
|
||||
FileContent: string(fileContent),
|
||||
})
|
||||
}
|
31
api/http/handler/hostmanagement/fdo/profile_list.go
Normal file
31
api/http/handler/hostmanagement/fdo/profile_list.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
// @id fdoProfileList
|
||||
// @summary retrieves all FDO profiles
|
||||
// @description retrieves all FDO profiles
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @failure 500 "Bad gateway"
|
||||
// @router /fdo/profiles [get]
|
||||
func (handler *Handler) fdoProfileList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
|
||||
profiles, err := handler.DataStore.FDOProfile().FDOProfiles()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
return response.JSON(w, profiles)
|
||||
}
|
67
api/http/handler/hostmanagement/fdo/profile_update.go
Normal file
67
api/http/handler/hostmanagement/fdo/profile_update.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// @id updateProfile
|
||||
// @summary updates an existing FDO Profile
|
||||
// @description updates an existing FDO Profile
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 409 "Profile name already exists"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo/profiles/{id} [put]
|
||||
func (handler *Handler) updateProfile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
id, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Bad request", errors.New("missing 'id' query parameter")}
|
||||
}
|
||||
|
||||
var payload createProfileFromFileContentPayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
profile, err := handler.DataStore.FDOProfile().FDOProfile(portainer.FDOProfileID(id))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a FDO Profile with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a FDO Profile with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
isUnique, err := handler.checkUniqueProfileName(payload.Name, id)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
if !isUnique {
|
||||
return &httperror.HandlerError{http.StatusConflict, fmt.Sprintf("A profile with the name '%s' already exists", payload.Name), errors.New("a profile already exists with this name")}
|
||||
}
|
||||
|
||||
filePath, err := handler.FileService.StoreFDOProfileFileFromBytes(strconv.Itoa(int(profile.ID)), []byte(payload.ProfileFileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update profile", err}
|
||||
}
|
||||
profile.FilePath = filePath
|
||||
profile.Name = payload.Name
|
||||
|
||||
err = handler.DataStore.FDOProfile().Update(profile.ID, profile)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update profile", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, profile)
|
||||
}
|
49
api/http/handler/hostmanagement/fdo/register.go
Normal file
49
api/http/handler/hostmanagement/fdo/register.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type registerDeviceResponse struct {
|
||||
Guid string `json:"guid" example:"c6ea3343-229a-4c07-9096-beef7134e1d3"`
|
||||
}
|
||||
|
||||
// @id fdoRegisterDevice
|
||||
// @summary register an FDO device
|
||||
// @description register an FDO device
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo/register [post]
|
||||
func (handler *Handler) fdoRegisterDevice(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
// Post a voucher
|
||||
ov, filename, err := request.RetrieveMultiPartFormFile(r, "voucher")
|
||||
if err != nil {
|
||||
logrus.WithField("filename", filename).WithError(err).Info("fdoRegisterDevice: readVoucher()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoRegisterDevice: read Voucher()", Err: err}
|
||||
}
|
||||
|
||||
fdoClient, err := handler.newFDOClient()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Info("fdoRegisterDevice: newFDOClient()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoRegisterDevice: newFDOClient()", Err: err}
|
||||
}
|
||||
|
||||
guid, err := fdoClient.PostVoucher(ov)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Info("fdoRegisterDevice: PostVoucher()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoRegisterDevice: PostVoucher()", Err: err}
|
||||
}
|
||||
|
||||
return response.JSON(w, registerDeviceResponse{guid})
|
||||
}
|
16
api/http/handler/hostmanagement/fdo/utils.go
Normal file
16
api/http/handler/hostmanagement/fdo/utils.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package fdo
|
||||
|
||||
func (handler *Handler) checkUniqueProfileName(name string, id int) (bool, error) {
|
||||
profiles, err := handler.DataStore.FDOProfile().FDOProfiles()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, profile := range profiles {
|
||||
if profile.Name == name && (id == -1 || id != int(profile.ID)) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue