mirror of https://github.com/portainer/portainer
				
				
				
			
		
			
				
	
	
		
			301 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Go
		
	
	
			
		
		
	
	
			301 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Go
		
	
	
package openamt
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"net/http"
 | 
						|
	"time"
 | 
						|
 | 
						|
	portainer "github.com/portainer/portainer/api"
 | 
						|
	"github.com/portainer/portainer/api/hostmanagement/openamt"
 | 
						|
	httperror "github.com/portainer/portainer/pkg/libhttp/error"
 | 
						|
	"github.com/portainer/portainer/pkg/libhttp/request"
 | 
						|
	"github.com/portainer/portainer/pkg/libhttp/response"
 | 
						|
 | 
						|
	"github.com/docker/docker/api/types/container"
 | 
						|
	"github.com/docker/docker/api/types/filters"
 | 
						|
	"github.com/docker/docker/api/types/image"
 | 
						|
	"github.com/docker/docker/api/types/network"
 | 
						|
	"github.com/docker/docker/client"
 | 
						|
	"github.com/rs/zerolog/log"
 | 
						|
	"github.com/segmentio/encoding/json"
 | 
						|
)
 | 
						|
 | 
						|
type HostInfo struct {
 | 
						|
	EndpointID     portainer.EndpointID `json:"EndpointID"`
 | 
						|
	RawOutput      string               `json:"RawOutput"`
 | 
						|
	AMT            string               `json:"AMT"`
 | 
						|
	UUID           string               `json:"UUID"`
 | 
						|
	DNSSuffix      string               `json:"DNS Suffix"`
 | 
						|
	BuildNumber    string               `json:"Build Number"`
 | 
						|
	ControlMode    string               `json:"Control Mode"`
 | 
						|
	ControlModeRaw int                  `json:"Control Mode (Raw)"`
 | 
						|
}
 | 
						|
 | 
						|
const (
 | 
						|
	// TODO: this should get extracted to some configurable - don't assume Docker Hub is everyone's global namespace, or that they're allowed to pull images from the internet
 | 
						|
	rpcGoImageName      = "ptrrd/openamt:rpc-go-json"
 | 
						|
	rpcGoContainerName  = "openamt-rpc-go"
 | 
						|
	dockerClientTimeout = 5 * time.Minute
 | 
						|
)
 | 
						|
 | 
						|
// @id OpenAMTHostInfo
 | 
						|
// @summary Request OpenAMT info from a node
 | 
						|
// @description Request OpenAMT info from a node
 | 
						|
// @description **Access policy**: administrator
 | 
						|
// @tags intel
 | 
						|
// @security jwt
 | 
						|
// @produce json
 | 
						|
// @param id path int true "Environment identifier"
 | 
						|
// @success 200 "Success"
 | 
						|
// @failure 400 "Invalid request"
 | 
						|
// @failure 403 "Permission denied to access settings"
 | 
						|
// @failure 500 "Server error"
 | 
						|
// @router /open_amt/{id}/info [get]
 | 
						|
func (handler *Handler) openAMTHostInfo(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
 | 
						|
	endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
 | 
						|
	if err != nil {
 | 
						|
		return httperror.BadRequest("Invalid environment identifier route variable", err)
 | 
						|
	}
 | 
						|
 | 
						|
	log.Info().Int("endpointID", endpointID).Msg("OpenAMTHostInfo")
 | 
						|
 | 
						|
	endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
 | 
						|
	if handler.DataStore.IsErrObjectNotFound(err) {
 | 
						|
		return httperror.NotFound("Unable to find an endpoint with the specified identifier inside the database", err)
 | 
						|
	} else if err != nil {
 | 
						|
		return httperror.InternalServerError("Unable to find an endpoint with the specified identifier inside the database", err)
 | 
						|
	}
 | 
						|
 | 
						|
	amtInfo, output, err := handler.getEndpointAMTInfo(endpoint)
 | 
						|
	if err != nil {
 | 
						|
		return httperror.InternalServerError(output, err)
 | 
						|
	}
 | 
						|
 | 
						|
	return response.JSON(w, amtInfo)
 | 
						|
}
 | 
						|
 | 
						|
func (handler *Handler) getEndpointAMTInfo(endpoint *portainer.Endpoint) (*HostInfo, string, error) {
 | 
						|
	ctx := context.TODO()
 | 
						|
 | 
						|
	// pull the image so we can check if there's a new one
 | 
						|
	// TODO: these should be able to be over-ridden (don't hardcode the assumption that secure users can access Docker Hub, or that its even the orchestrator's "global namespace")
 | 
						|
	cmdLine := []string{"amtinfo", "--json"}
 | 
						|
	output, err := handler.PullAndRunContainer(ctx, endpoint, rpcGoImageName, rpcGoContainerName, cmdLine)
 | 
						|
	if err != nil {
 | 
						|
		return nil, output, err
 | 
						|
	}
 | 
						|
 | 
						|
	amtInfo := HostInfo{}
 | 
						|
	_ = json.Unmarshal([]byte(output), &amtInfo)
 | 
						|
 | 
						|
	amtInfo.EndpointID = endpoint.ID
 | 
						|
	amtInfo.RawOutput = output
 | 
						|
 | 
						|
	return &amtInfo, "", nil
 | 
						|
}
 | 
						|
 | 
						|
func (handler *Handler) PullAndRunContainer(ctx context.Context, endpoint *portainer.Endpoint, imageName, containerName string, cmdLine []string) (output string, err error) {
 | 
						|
	// TODO: this should not be Docker specific
 | 
						|
	// TODO: extract from this Handler into something global.
 | 
						|
 | 
						|
	// TODO: start
 | 
						|
	//      docker run --rm -it --privileged 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 := ""
 | 
						|
	timeout := dockerClientTimeout
 | 
						|
	docker, err := handler.DockerClientFactory.CreateClient(endpoint, nodeName, &timeout)
 | 
						|
	if err != nil {
 | 
						|
		return "Unable to create Docker Client connection", err
 | 
						|
	}
 | 
						|
	defer docker.Close()
 | 
						|
 | 
						|
	if err := pullImage(ctx, docker, imageName); err != nil {
 | 
						|
		return "Could not pull image from registry", err
 | 
						|
	}
 | 
						|
 | 
						|
	output, err = runContainer(ctx, docker, imageName, containerName, cmdLine)
 | 
						|
	if err != nil {
 | 
						|
		return "Could not run container", err
 | 
						|
	}
 | 
						|
 | 
						|
	return output, nil
 | 
						|
}
 | 
						|
 | 
						|
// TODO: ideally, pullImage and runContainer will become a simple version of the use compose abstraction that can be called from withing Portainer.
 | 
						|
// TODO: the idea being that if we have an internal struct of a parsed compose file, we can also populate that struct programmatically, and run it to get the result I'm getting here.
 | 
						|
// TODO: likely an upgrade and abstraction of DeployComposeStack/DeploySwarmStack/DeployKubernetesStack
 | 
						|
// pullImage will pull the image to the specified environment
 | 
						|
// TODO: add k8s implementation
 | 
						|
// TODO: work out registry auth
 | 
						|
func pullImage(ctx context.Context, docker *client.Client, imageName string) error {
 | 
						|
	out, err := docker.ImagePull(ctx, imageName, image.PullOptions{})
 | 
						|
	if err != nil {
 | 
						|
		log.Error().Str("image_name", imageName).Err(err).Msg("could not pull image from registry")
 | 
						|
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	defer out.Close()
 | 
						|
	outputBytes, err := io.ReadAll(out)
 | 
						|
	if err != nil {
 | 
						|
		log.Error().Str("image_name", imageName).Err(err).Msg("could not read image pull output")
 | 
						|
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	log.Debug().Str("image_name", imageName).Str("output", string(outputBytes)).Msg("image pulled")
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// TODO: ideally, pullImage and runContainer will become a simple version of the use compose abstraction that can be called from withing Portainer.
 | 
						|
// runContainer should be used to run a short command that returns information to stdout
 | 
						|
// TODO: add k8s support
 | 
						|
func runContainer(ctx context.Context, docker *client.Client, imageName, containerName string, cmdLine []string) (output string, err error) {
 | 
						|
	opts := container.ListOptions{All: true}
 | 
						|
	opts.Filters = filters.NewArgs()
 | 
						|
	opts.Filters.Add("name", containerName)
 | 
						|
	existingContainers, err := docker.ContainerList(ctx, opts)
 | 
						|
	if err != nil {
 | 
						|
		log.Error().
 | 
						|
			Str("image_name", imageName).
 | 
						|
			Str("container_name", containerName).
 | 
						|
			Err(err).
 | 
						|
			Msg("listing existing container")
 | 
						|
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
 | 
						|
	if len(existingContainers) > 0 {
 | 
						|
		err = docker.ContainerRemove(ctx, existingContainers[0].ID, container.RemoveOptions{Force: true})
 | 
						|
		if err != nil {
 | 
						|
			log.Error().
 | 
						|
				Str("image_name", imageName).
 | 
						|
				Str("container_name", containerName).
 | 
						|
				Err(err).
 | 
						|
				Msg("removing existing container")
 | 
						|
 | 
						|
			return "", err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	created, err := docker.ContainerCreate(
 | 
						|
		ctx,
 | 
						|
		&container.Config{
 | 
						|
			Image:        imageName,
 | 
						|
			Cmd:          cmdLine,
 | 
						|
			Env:          []string{},
 | 
						|
			Tty:          true,
 | 
						|
			OpenStdin:    true,
 | 
						|
			AttachStdout: true,
 | 
						|
			AttachStderr: true,
 | 
						|
		},
 | 
						|
		&container.HostConfig{
 | 
						|
			Privileged: true,
 | 
						|
		},
 | 
						|
		&network.NetworkingConfig{},
 | 
						|
		nil,
 | 
						|
		containerName,
 | 
						|
	)
 | 
						|
 | 
						|
	if err != nil {
 | 
						|
		log.Error().
 | 
						|
			Str("image_name", imageName).
 | 
						|
			Str("container_name", containerName).
 | 
						|
			Err(err).
 | 
						|
			Msg("creating container")
 | 
						|
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
 | 
						|
	err = docker.ContainerStart(ctx, created.ID, container.StartOptions{})
 | 
						|
	if err != nil {
 | 
						|
		log.Error().
 | 
						|
			Str("image_name", imageName).
 | 
						|
			Str("container_name", containerName).
 | 
						|
			Err(err).
 | 
						|
			Msg("starting container")
 | 
						|
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
 | 
						|
	log.Debug().Str("container_name", containerName).Msg("container created and started")
 | 
						|
 | 
						|
	statusCh, errCh := docker.ContainerWait(ctx, created.ID, container.WaitConditionNotRunning)
 | 
						|
	var statusCode int64
 | 
						|
	select {
 | 
						|
	case err := <-errCh:
 | 
						|
		if err != nil {
 | 
						|
			log.Error().
 | 
						|
				Str("image_name", imageName).
 | 
						|
				Str("container_name", containerName).
 | 
						|
				Err(err).
 | 
						|
				Msg("starting container")
 | 
						|
 | 
						|
			return "", err
 | 
						|
		}
 | 
						|
	case status := <-statusCh:
 | 
						|
		statusCode = status.StatusCode
 | 
						|
	}
 | 
						|
 | 
						|
	log.Debug().Int64("status", statusCode).Msg("container wait status")
 | 
						|
 | 
						|
	out, err := docker.ContainerLogs(ctx, created.ID, container.LogsOptions{ShowStdout: true})
 | 
						|
	if err != nil {
 | 
						|
		log.Error().Err(err).Str("image_name", imageName).Str("container_name", containerName).Msg("getting container log")
 | 
						|
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
 | 
						|
	err = docker.ContainerRemove(ctx, created.ID, container.RemoveOptions{})
 | 
						|
	if err != nil {
 | 
						|
		log.Error().
 | 
						|
			Str("image_name", imageName).
 | 
						|
			Str("container_name", containerName).
 | 
						|
			Err(err).
 | 
						|
			Msg("removing container")
 | 
						|
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
 | 
						|
	outputBytes, err := io.ReadAll(out)
 | 
						|
	if err != nil {
 | 
						|
		log.Error().
 | 
						|
			Str("image_name", imageName).
 | 
						|
			Str("container_name", containerName).
 | 
						|
			Err(err).
 | 
						|
			Msg("read container output")
 | 
						|
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
 | 
						|
	log.Debug().
 | 
						|
		Str("container_name", containerName).
 | 
						|
		Str("output", string(outputBytes)).
 | 
						|
		Msg("container finished with output")
 | 
						|
 | 
						|
	return string(outputBytes), nil
 | 
						|
}
 | 
						|
 | 
						|
func (handler *Handler) activateDevice(endpoint *portainer.Endpoint, settings portainer.Settings) error {
 | 
						|
	ctx := context.TODO()
 | 
						|
 | 
						|
	config := settings.OpenAMTConfiguration
 | 
						|
	cmdLine := []string{
 | 
						|
		"activate",
 | 
						|
		"-n",
 | 
						|
		"-v",
 | 
						|
		"-u", fmt.Sprintf("wss://%s/activate", config.MPSServer),
 | 
						|
		"-profile", openamt.DefaultProfileName,
 | 
						|
		"-d", config.DomainName,
 | 
						|
		"-password", config.MPSPassword,
 | 
						|
	}
 | 
						|
 | 
						|
	_, err := handler.PullAndRunContainer(ctx, endpoint, rpcGoImageName, rpcGoContainerName, cmdLine)
 | 
						|
 | 
						|
	return err
 | 
						|
}
 |