mirror of
https://github.com/lxc/incus
synced 2026-08-02 05:26:46 +00:00
The fingerprint validation added for CVE-2026-48769 was only applied to the direct protocol. Validate it for all protocols and re-check after it is taken from the remote server's response. This addresses GHSA-p2v3-6wvc-cv3p (CVE pending) Signed-off-by: Stéphane Graber <stgraber@stgraber.org>
683 lines
20 KiB
Go
683 lines
20 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"slices"
|
|
"time"
|
|
|
|
incus "github.com/lxc/incus/v7/client"
|
|
internalIO "github.com/lxc/incus/v7/internal/io"
|
|
"github.com/lxc/incus/v7/internal/server/db"
|
|
"github.com/lxc/incus/v7/internal/server/db/cluster"
|
|
"github.com/lxc/incus/v7/internal/server/locking"
|
|
"github.com/lxc/incus/v7/internal/server/operations"
|
|
"github.com/lxc/incus/v7/internal/server/project"
|
|
"github.com/lxc/incus/v7/internal/server/response"
|
|
"github.com/lxc/incus/v7/internal/server/state"
|
|
localUtil "github.com/lxc/incus/v7/internal/server/util"
|
|
internalUtil "github.com/lxc/incus/v7/internal/util"
|
|
"github.com/lxc/incus/v7/internal/version"
|
|
"github.com/lxc/incus/v7/shared/api"
|
|
"github.com/lxc/incus/v7/shared/cancel"
|
|
"github.com/lxc/incus/v7/shared/ioprogress"
|
|
"github.com/lxc/incus/v7/shared/logger"
|
|
"github.com/lxc/incus/v7/shared/units"
|
|
"github.com/lxc/incus/v7/shared/util"
|
|
"github.com/lxc/incus/v7/shared/validate"
|
|
)
|
|
|
|
// imageDownloadArgs used with imageDownload.
|
|
type imageDownloadArgs struct {
|
|
ProjectName string
|
|
Server string
|
|
Protocol string
|
|
Certificate string
|
|
Secret string
|
|
Alias string
|
|
Type string
|
|
SetCached bool
|
|
PreferCached bool
|
|
AutoUpdate bool
|
|
Public bool
|
|
StoragePool string
|
|
Budget int64
|
|
SourceProjectName string
|
|
}
|
|
|
|
// imageOperationLock acquires a lock for operating on an image and returns the unlock function.
|
|
func imageOperationLock(ctx context.Context, fingerprint string) (locking.UnlockFunc, error) {
|
|
l := logger.AddContext(logger.Ctx{"fingerprint": fingerprint})
|
|
l.Debug("Acquiring lock for image")
|
|
defer l.Debug("Lock acquired for image")
|
|
|
|
return locking.Lock(ctx, fmt.Sprintf("ImageOperation_%s", fingerprint))
|
|
}
|
|
|
|
// imageDownload resolves the image fingerprint and if not in the database, downloads it.
|
|
func imageDownload(ctx context.Context, r *http.Request, s *state.State, op *operations.Operation, args *imageDownloadArgs) (*api.Image, bool, error) {
|
|
var err error
|
|
var ctxMap logger.Ctx
|
|
|
|
var remote incus.ImageServer
|
|
var info *api.Image
|
|
|
|
// Check if the project allows retrieving the image.
|
|
err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
|
|
err := project.AllowImageDownload(tx, args.ProjectName, args.Server)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
// Default protocol is Incus. Copy so that local modifications aren't propagated to args.
|
|
protocol := args.Protocol
|
|
if protocol == "" {
|
|
protocol = "incus"
|
|
}
|
|
|
|
// Copy so that local modifications aren't propagated to args.
|
|
alias := args.Alias
|
|
|
|
// Default the fingerprint to the alias string we received
|
|
fp := alias
|
|
|
|
// Attempt to resolve the alias
|
|
if args.Server != "" && slices.Contains([]string{"incus", "lxd", "oci", "simplestreams"}, protocol) {
|
|
clientArgs := &incus.ConnectionArgs{
|
|
TLSServerCert: args.Certificate,
|
|
UserAgent: version.UserAgent,
|
|
Proxy: s.Proxy,
|
|
CachePath: s.OS.CacheDir,
|
|
CacheExpiry: time.Hour,
|
|
SkipGetEvents: true,
|
|
SkipGetServer: true,
|
|
TempPath: internalUtil.VarPath("images"),
|
|
}
|
|
|
|
if slices.Contains([]string{"incus", "lxd"}, protocol) {
|
|
// Setup client
|
|
remote, err = incus.ConnectPublicIncus(args.Server, clientArgs)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("Failed to connect to the server %q: %w", args.Server, err)
|
|
}
|
|
|
|
server, ok := remote.(incus.InstanceServer)
|
|
if ok {
|
|
remote = server.UseProject(args.SourceProjectName)
|
|
}
|
|
} else if protocol == "oci" {
|
|
// Setup OCI client
|
|
remote, err = incus.ConnectOCI(args.Server, clientArgs)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("Failed to connect to oci server %q: %w", args.Server, err)
|
|
}
|
|
} else if protocol == "simplestreams" {
|
|
// Setup simplestreams client
|
|
remote, err = incus.ConnectSimpleStreams(args.Server, clientArgs)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("Failed to connect to simple streams server %q: %w", args.Server, err)
|
|
}
|
|
}
|
|
|
|
// For public images, handle aliases and initial metadata
|
|
if args.Secret == "" {
|
|
// Look for a matching alias. Note, this err message is lost!
|
|
entry, _, err := remote.GetImageAliasType(args.Type, fp)
|
|
if err == nil {
|
|
fp = entry.Target
|
|
} else if args.Type != "" {
|
|
// If no match was found for the requested type, check whether
|
|
// the other instance type has one to give a more helpful hint.
|
|
otherType := "virtual-machine"
|
|
if args.Type == "virtual-machine" {
|
|
otherType = "container"
|
|
}
|
|
|
|
_, _, otherErr := remote.GetImageAliasType(otherType, fp)
|
|
if otherErr == nil {
|
|
return nil, false, fmt.Errorf("The requested image couldn't be found for instance type %q, but one was found for instance type %q", args.Type, otherType)
|
|
}
|
|
}
|
|
|
|
// Expand partial fingerprints
|
|
info, _, err = remote.GetImage(fp)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("Failed getting remote image info: %w", err)
|
|
}
|
|
|
|
fp = info.Fingerprint
|
|
}
|
|
}
|
|
|
|
// Ensure we are the only ones operating on this image.
|
|
unlock, err := imageOperationLock(ctx, fp)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
defer unlock()
|
|
|
|
// If auto-update is on and we're being given the image by
|
|
// alias, try to use a locally cached image matching the given
|
|
// server/protocol/alias, regardless of whether it's stale or
|
|
// not (we can assume that it will be not *too* stale since
|
|
// auto-update is on).
|
|
interval := s.GlobalConfig.ImagesAutoUpdateIntervalHours()
|
|
|
|
if args.PreferCached && interval > 0 && alias != fp {
|
|
err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
|
|
for _, architecture := range s.OS.Architectures {
|
|
cachedFingerprint, err := tx.GetCachedImageSourceFingerprint(ctx, args.Server, args.Protocol, alias, args.Type, architecture)
|
|
if err == nil && cachedFingerprint != fp {
|
|
fp = cachedFingerprint
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
}
|
|
|
|
var imgInfo *api.Image
|
|
|
|
err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
|
|
// Check if the image already exists in this project (partial hash match).
|
|
_, imgInfo, err = tx.GetImage(ctx, fp, cluster.ImageFilter{Project: &args.ProjectName})
|
|
|
|
return err
|
|
})
|
|
if err == nil {
|
|
var nodeAddress string
|
|
|
|
err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
|
|
// Check if the image is available locally or it's on another node.
|
|
nodeAddress, err = tx.LocateImage(ctx, imgInfo.Fingerprint)
|
|
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("Failed locating image %q in the cluster: %w", imgInfo.Fingerprint, err)
|
|
}
|
|
|
|
if nodeAddress != "" {
|
|
// The image is available from another node, let's try to import it.
|
|
err = instanceImageTransfer(s, r, args.ProjectName, imgInfo.Fingerprint, nodeAddress)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("Failed transferring image %q from %q: %w", imgInfo.Fingerprint, nodeAddress, err)
|
|
}
|
|
|
|
err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
|
|
// As the image record already exists in the project, just add the node ID to the image.
|
|
return tx.AddImageToLocalNode(ctx, args.ProjectName, imgInfo.Fingerprint)
|
|
})
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("Failed adding transferred image %q to local cluster member: %w", imgInfo.Fingerprint, err)
|
|
}
|
|
}
|
|
} else if response.IsNotFoundError(err) {
|
|
err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
|
|
// Check if the image already exists in some other project.
|
|
_, imgInfo, err = tx.GetImageFromAnyProject(ctx, fp)
|
|
|
|
return err
|
|
})
|
|
if err == nil {
|
|
var nodeAddress string
|
|
|
|
err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
|
|
// Check if the image is available locally or it's on another node. Do this before creating
|
|
// the missing DB record so we don't include ourself in the search results.
|
|
nodeAddress, err = tx.LocateImage(ctx, imgInfo.Fingerprint)
|
|
if err != nil {
|
|
return fmt.Errorf("Locate image %q in the cluster: %w", imgInfo.Fingerprint, err)
|
|
}
|
|
|
|
// We need to insert the database entry for this project, including the node ID entry.
|
|
err = tx.CreateImage(ctx, args.ProjectName, imgInfo.Fingerprint, imgInfo.Filename, imgInfo.Size, args.Public, imgInfo.AutoUpdate, imgInfo.Architecture, imgInfo.CreatedAt, imgInfo.ExpiresAt, imgInfo.Properties, imgInfo.Type, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed creating image record for project: %w", err)
|
|
}
|
|
|
|
// Mark the image as "cached" if downloading for an instance.
|
|
if args.SetCached {
|
|
err = tx.SetImageCachedAndLastUseDate(ctx, args.ProjectName, imgInfo.Fingerprint, time.Now().UTC())
|
|
if err != nil {
|
|
return fmt.Errorf("Failed setting cached flag and last use date: %w", err)
|
|
}
|
|
}
|
|
|
|
var id int
|
|
|
|
id, imgInfo, err = tx.GetImage(ctx, fp, cluster.ImageFilter{Project: &args.ProjectName})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return tx.CreateImageSource(ctx, id, args.Server, args.Protocol, args.Certificate, alias)
|
|
})
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
// Transfer image if needed (after database record has been created above).
|
|
if nodeAddress != "" {
|
|
// The image is available from another node, let's try to import it.
|
|
err = instanceImageTransfer(s, r, args.ProjectName, info.Fingerprint, nodeAddress)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("Failed transferring image: %w", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if imgInfo != nil {
|
|
info = imgInfo
|
|
ctxMap = logger.Ctx{"fingerprint": info.Fingerprint}
|
|
logger.Debug("Image already exists in the DB", ctxMap)
|
|
|
|
// If not requested in a particular pool, we're done.
|
|
if args.StoragePool == "" {
|
|
return info, false, nil
|
|
}
|
|
|
|
ctxMap["pool"] = args.StoragePool
|
|
|
|
var poolID int64
|
|
var poolIDs []int64
|
|
|
|
err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
|
|
// Get the ID of the target storage pool.
|
|
poolID, err = tx.GetStoragePoolID(ctx, args.StoragePool)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check if the image is already in the pool.
|
|
poolIDs, err = tx.GetPoolsWithImage(ctx, info.Fingerprint)
|
|
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
if slices.Contains(poolIDs, poolID) {
|
|
logger.Debug("Image already exists on storage pool", ctxMap)
|
|
return info, false, nil
|
|
}
|
|
|
|
// Import the image in the pool.
|
|
logger.Debug("Image does not exist on storage pool", ctxMap)
|
|
|
|
err = imageCreateInPool(s, info, args.StoragePool)
|
|
if err != nil {
|
|
ctxMap["err"] = err
|
|
logger.Debug("Failed to create image on storage pool", ctxMap)
|
|
return nil, false, fmt.Errorf("Failed to create image %q on storage pool %q: %w", info.Fingerprint, args.StoragePool, err)
|
|
}
|
|
|
|
logger.Debug("Created image on storage pool", ctxMap)
|
|
return info, false, nil
|
|
}
|
|
|
|
// Begin downloading
|
|
if op == nil {
|
|
ctxMap = logger.Ctx{"alias": alias, "server": args.Server}
|
|
} else {
|
|
ctxMap = logger.Ctx{"trigger": op.URL(), "fingerprint": fp, "operation": op.ID(), "alias": alias, "server": args.Server}
|
|
}
|
|
|
|
logger.Info("Downloading image", ctxMap)
|
|
|
|
// The fingerprint is used as a file name, so reject anything that isn't a
|
|
// partial or full hex fingerprint to avoid path traversal. It's re-validated
|
|
// as a full SHA-256 once expanded from the remote below.
|
|
match, _ := regexp.MatchString("^[0-9a-f]{1,64}$", fp)
|
|
if !match {
|
|
return nil, false, errors.New("Invalid image fingerprint")
|
|
}
|
|
|
|
// Cleanup any leftover from a past attempt
|
|
destDir := internalUtil.VarPath("images")
|
|
destName := filepath.Join(destDir, fp)
|
|
|
|
failure := true
|
|
cleanup := func() {
|
|
if failure {
|
|
_ = os.Remove(destName)
|
|
_ = os.Remove(destName + ".rootfs")
|
|
}
|
|
}
|
|
defer cleanup()
|
|
|
|
// Setup a progress handler
|
|
progress := func(progress ioprogress.ProgressData) {
|
|
if op == nil {
|
|
return
|
|
}
|
|
|
|
meta := op.Metadata()
|
|
if meta == nil {
|
|
meta = make(map[string]any)
|
|
}
|
|
|
|
if meta["download_progress"] != progress.Text {
|
|
_ = op.ExtendMetadata(map[string]any{"download_progress": progress.Text})
|
|
}
|
|
}
|
|
|
|
var canceler *cancel.HTTPRequestCanceller
|
|
if op != nil {
|
|
canceler = cancel.NewHTTPRequestCanceller()
|
|
op.SetCanceler(canceler)
|
|
}
|
|
|
|
if slices.Contains([]string{"incus", "lxd", "oci", "simplestreams"}, protocol) {
|
|
// Create the target files
|
|
dest, err := os.Create(destName)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
defer logger.WarnOnErrorExcept(dest.Close, []error{os.ErrClosed}, "Failed to close image file")
|
|
|
|
destRootfs, err := os.Create(destName + ".rootfs")
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
defer logger.WarnOnErrorExcept(destRootfs.Close, []error{os.ErrClosed}, "Failed to close rootfs file")
|
|
|
|
// Get the image information
|
|
if info == nil {
|
|
if args.Secret != "" {
|
|
info, _, err = remote.GetPrivateImage(fp, args.Secret)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
// Expand the fingerprint now and mark alias string to match
|
|
fp = info.Fingerprint
|
|
alias = info.Fingerprint
|
|
} else {
|
|
info, _, err = remote.GetImage(fp)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compatibility with older servers
|
|
if info.Type == "" {
|
|
info.Type = "container"
|
|
}
|
|
|
|
if args.Budget > 0 && info.Size > args.Budget {
|
|
return nil, false, fmt.Errorf("Remote image with size %d exceeds allowed bugdget of %d", info.Size, args.Budget)
|
|
}
|
|
|
|
// Download the image
|
|
var resp *incus.ImageFileResponse
|
|
request := incus.ImageFileRequest{
|
|
MetaFile: io.ReadWriteSeeker(dest),
|
|
RootfsFile: io.ReadWriteSeeker(destRootfs),
|
|
ProgressHandler: progress,
|
|
Canceler: canceler,
|
|
DeltaSourceRetriever: func(fingerprint string, file string) string {
|
|
path := internalUtil.VarPath("images", fmt.Sprintf("%s.%s", fingerprint, file))
|
|
if util.PathExists(path) {
|
|
return path
|
|
}
|
|
|
|
return ""
|
|
},
|
|
}
|
|
|
|
if args.Secret != "" {
|
|
resp, err = remote.GetPrivateImageFile(fp, args.Secret, request)
|
|
} else {
|
|
resp, err = remote.GetImageFile(fp, request)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
// Truncate down to size
|
|
if resp.RootfsSize > 0 {
|
|
err = destRootfs.Truncate(resp.RootfsSize)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
}
|
|
|
|
err = dest.Truncate(resp.MetaSize)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
// Deal with unified images
|
|
if resp.RootfsSize == 0 {
|
|
err := os.Remove(destName + ".rootfs")
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
}
|
|
|
|
err = dest.Close()
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
err = destRootfs.Close()
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
} else if protocol == "direct" {
|
|
// Setup HTTP client
|
|
httpClient, err := localUtil.HTTPClient(args.Certificate, s.Proxy)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
// Use relatively short response header timeout so as not to hold the image lock open too long.
|
|
httpTransport, ok := httpClient.Transport.(*http.Transport)
|
|
if !ok {
|
|
return nil, false, errors.New("Unexpected HTTP transport type")
|
|
}
|
|
|
|
httpTransport.ResponseHeaderTimeout = 30 * time.Second
|
|
|
|
req, err := http.NewRequest("GET", args.Server, nil)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
req.Header.Set("User-Agent", version.UserAgent)
|
|
|
|
// Make the request
|
|
raw, doneCh, err := cancel.CancelableDownload(canceler, httpClient.Do, req)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
defer close(doneCh)
|
|
|
|
if raw.StatusCode != http.StatusOK {
|
|
return nil, false, fmt.Errorf("Unable to fetch %q: %s", args.Server, raw.Status)
|
|
}
|
|
|
|
// Progress handler
|
|
body := &ioprogress.ProgressReader{
|
|
ReadCloser: raw.Body,
|
|
Tracker: &ioprogress.ProgressTracker{
|
|
Length: raw.ContentLength,
|
|
Handler: func(percent int64, speed int64) {
|
|
progress(ioprogress.ProgressData{Text: fmt.Sprintf("%d%% (%s/s)", percent, units.GetByteSizeString(speed, 2))})
|
|
},
|
|
},
|
|
}
|
|
|
|
// Create the target files
|
|
f, err := os.Create(destName)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
defer logger.WarnOnErrorExcept(f.Close, []error{os.ErrClosed}, "Failed to close image file")
|
|
|
|
// Hashing
|
|
hash256 := sha256.New()
|
|
|
|
// Download the image
|
|
writer := internalIO.NewQuotaWriter(io.MultiWriter(f, hash256), args.Budget)
|
|
size, err := util.SafeCopy(writer, body)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
// Validate hash
|
|
result := fmt.Sprintf("%x", hash256.Sum(nil))
|
|
if result != fp {
|
|
return nil, false, fmt.Errorf("Hash mismatch for %q: %s != %s", args.Server, result, fp)
|
|
}
|
|
|
|
// Parse the image
|
|
imageMeta, imageType, err := getImageMetadata(destName)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
info = &api.Image{}
|
|
info.Fingerprint = fp
|
|
info.Size = size
|
|
info.Architecture = imageMeta.Architecture
|
|
info.Properties = imageMeta.Properties
|
|
info.Type = imageType
|
|
|
|
if imageMeta.CreationDate > 0 {
|
|
info.CreatedAt = time.Unix(imageMeta.CreationDate, 0)
|
|
}
|
|
|
|
if imageMeta.ExpiryDate > 0 {
|
|
info.ExpiresAt = time.Unix(imageMeta.ExpiryDate, 0)
|
|
}
|
|
|
|
err = f.Close()
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
} else {
|
|
return nil, false, fmt.Errorf("Unsupported protocol: %v", protocol)
|
|
}
|
|
|
|
// Override visibility
|
|
info.Public = args.Public
|
|
|
|
// We want to enable auto-update only if we were passed an
|
|
// alias name, so we can figure when the associated
|
|
// fingerprint changes in the remote.
|
|
if alias != fp {
|
|
info.AutoUpdate = args.AutoUpdate
|
|
}
|
|
|
|
err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
|
|
// Create the database entry
|
|
return tx.CreateImage(ctx, args.ProjectName, info.Fingerprint, info.Filename, info.Size, info.Public, info.AutoUpdate, info.Architecture, info.CreatedAt, info.ExpiresAt, info.Properties, info.Type, nil)
|
|
})
|
|
if err != nil && api.StatusErrorCheck(err, http.StatusConflict) {
|
|
// Another cluster member created the record concurrently, reuse it and just register this member.
|
|
err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
|
|
_, info, err = tx.GetImage(ctx, info.Fingerprint, cluster.ImageFilter{Project: &args.ProjectName})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return tx.AddImageToLocalNode(ctx, args.ProjectName, info.Fingerprint)
|
|
})
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("Failed creating image record: %w", err)
|
|
}
|
|
|
|
// Image is in the DB now, don't wipe on-disk files on failure
|
|
failure = false
|
|
|
|
// Re-validate the fingerprint as it may have been updated from the remote
|
|
// server's response, and it is used as a file name below.
|
|
err = validate.IsSHA256(fp)
|
|
if err != nil {
|
|
return nil, false, errors.New("Invalid image fingerprint")
|
|
}
|
|
|
|
// Check if the image path changed (private images)
|
|
newDestName := filepath.Join(destDir, fp)
|
|
if newDestName != destName {
|
|
err = internalUtil.FileMove(destName, newDestName)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
if util.PathExists(destName + ".rootfs") {
|
|
err = internalUtil.FileMove(destName+".rootfs", newDestName+".rootfs")
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Record the image source
|
|
if alias != fp {
|
|
err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
|
|
id, _, err := tx.GetImage(ctx, fp, cluster.ImageFilter{Project: &args.ProjectName})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return tx.CreateImageSource(ctx, id, args.Server, protocol, args.Certificate, alias)
|
|
})
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
}
|
|
|
|
// Import into the requested storage pool
|
|
if args.StoragePool != "" {
|
|
err = imageCreateInPool(s, info, args.StoragePool)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
}
|
|
|
|
// Mark the image as "cached" if downloading for an instance
|
|
if args.SetCached {
|
|
err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
|
|
return tx.SetImageCachedAndLastUseDate(ctx, args.ProjectName, fp, time.Now().UTC())
|
|
})
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("Failed setting cached flag and last use date: %w", err)
|
|
}
|
|
}
|
|
|
|
logger.Info("Image downloaded", ctxMap)
|
|
|
|
return info, true, nil
|
|
}
|