github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/container/lxd/image.go (about)

     1  // Copyright 2018 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package lxd
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"path"
    10  	"strings"
    11  	"time"
    12  
    13  	lxd "github.com/canonical/lxd/client"
    14  	"github.com/canonical/lxd/shared/api"
    15  	"github.com/juju/errors"
    16  	"github.com/juju/retry"
    17  
    18  	jujuarch "github.com/juju/juju/core/arch"
    19  	jujubase "github.com/juju/juju/core/base"
    20  	"github.com/juju/juju/core/instance"
    21  	jujuos "github.com/juju/juju/core/os"
    22  	"github.com/juju/juju/core/status"
    23  	"github.com/juju/juju/environs"
    24  )
    25  
    26  // SourcedImage is the result of a successful image acquisition.
    27  // It includes the relevant data that located the image.
    28  type SourcedImage struct {
    29  	// Image is the actual image data that was located.
    30  	Image *api.Image
    31  	// LXDServer is the image server that supplied the image.
    32  	LXDServer lxd.ImageServer
    33  }
    34  
    35  // FindImage searches the input sources in supplied order, looking for an OS
    36  // image matching the supplied base and architecture.
    37  // If found, the image and the server from which it was acquired are returned.
    38  // If the server is remote the image will be cached by LXD when used to create
    39  // a container.
    40  // Supplying true for copyLocal will copy the image to the local cache.
    41  // Copied images will have the juju/series/arch alias added to them.
    42  // The callback argument is used to report copy progress.
    43  func (s *Server) FindImage(
    44  	ctx context.Context,
    45  	base jujubase.Base,
    46  	arch string,
    47  	virtType instance.VirtType,
    48  	sources []ServerSpec,
    49  	copyLocal bool,
    50  	callback environs.StatusCallbackFunc,
    51  ) (SourcedImage, error) {
    52  	if callback != nil {
    53  		_ = callback(status.Provisioning, "acquiring LXD image", nil)
    54  	}
    55  
    56  	// First we check if we have the image locally.
    57  	localAlias := baseLocalAlias(base.DisplayString(), arch, virtType)
    58  	var target string
    59  	entry, _, err := s.GetImageAlias(localAlias)
    60  	if err != nil && !IsLXDNotFound(err) {
    61  		return SourcedImage{}, errors.Trace(err)
    62  	}
    63  
    64  	if entry != nil {
    65  		// We already have an image with the given alias, so just use that.
    66  		target = entry.Target
    67  		image, _, err := s.GetImage(target)
    68  		if err == nil && isCompatibleVirtType(virtType, image.Type) {
    69  			logger.Debugf("Found image locally - %q %q", image.Filename, target)
    70  			return SourcedImage{
    71  				Image:     image,
    72  				LXDServer: s.InstanceServer,
    73  			}, nil
    74  		}
    75  	}
    76  
    77  	sourced := SourcedImage{}
    78  	lastErr := fmt.Errorf("no matching image found")
    79  
    80  	// We don't have an image locally with the juju-specific alias,
    81  	// so look in each of the provided remote sources for any of the aliases
    82  	// that might identify the image we want.
    83  	aliases, err := baseRemoteAliases(base, arch)
    84  	if err != nil {
    85  		return sourced, errors.Trace(err)
    86  	}
    87  	for _, remote := range sources {
    88  		source, err := ConnectImageRemote(ctx, remote)
    89  		if err != nil {
    90  			logger.Infof("failed to connect to %q: %s", remote.Host, err)
    91  			lastErr = errors.Trace(err)
    92  			continue
    93  		}
    94  		for _, alias := range aliases {
    95  			if res, _, err := source.GetImageAliasType(string(virtType), alias); err == nil && res != nil && res.Target != "" {
    96  				target = res.Target
    97  				break
    98  			}
    99  		}
   100  		if target != "" {
   101  			image, _, err := source.GetImage(target)
   102  			if err == nil {
   103  				logger.Debugf("Found image remotely - %q %q %q", remote.Name, image.Filename, target)
   104  				sourced.Image = image
   105  				sourced.LXDServer = source
   106  				break
   107  			} else {
   108  				lastErr = errors.Trace(err)
   109  			}
   110  		}
   111  	}
   112  
   113  	if sourced.Image == nil {
   114  		return sourced, lastErr
   115  	}
   116  
   117  	// If requested, copy the image to the local cache, adding the local alias.
   118  	if copyLocal {
   119  		if err := s.CopyRemoteImage(ctx, sourced, []string{localAlias}, callback); err != nil {
   120  			return sourced, errors.Trace(err)
   121  		}
   122  
   123  		// Now that we have the image cached locally, we indicate in the return
   124  		// that the source is local instead of the remote where we found it.
   125  		sourced.LXDServer = s.InstanceServer
   126  	}
   127  
   128  	return sourced, nil
   129  }
   130  
   131  // CopyRemoteImage accepts an image sourced from a remote server and copies it
   132  // to the local cache
   133  func (s *Server) CopyRemoteImage(
   134  	ctx context.Context, sourced SourcedImage, aliases []string, callback environs.StatusCallbackFunc,
   135  ) error {
   136  	logger.Debugf("Copying image from remote server")
   137  
   138  	newAliases := make([]api.ImageAlias, len(aliases))
   139  	for i, a := range aliases {
   140  		newAliases[i] = api.ImageAlias{Name: a}
   141  	}
   142  	req := &lxd.ImageCopyArgs{Aliases: newAliases}
   143  	progress := func(op api.Operation) {
   144  		if op.Metadata == nil {
   145  			return
   146  		}
   147  		for _, key := range []string{"fs_progress", "download_progress"} {
   148  			if value, ok := op.Metadata[key]; ok {
   149  				_ = callback(status.Provisioning, fmt.Sprintf("Retrieving image: %s", value.(string)), nil)
   150  				return
   151  			}
   152  		}
   153  	}
   154  
   155  	var op lxd.RemoteOperation
   156  	attemptDownload := func() error {
   157  		var err error
   158  		op, err = s.CopyImage(sourced.LXDServer, *sourced.Image, req)
   159  		if err != nil {
   160  			return err
   161  		}
   162  		// Report progress via callback if supplied.
   163  		if callback != nil {
   164  			_, err = op.AddHandler(progress)
   165  			if err != nil {
   166  				return err
   167  			}
   168  		}
   169  		if err := op.Wait(); err != nil {
   170  			return err
   171  		}
   172  		return nil
   173  	}
   174  	// NOTE(jack-w-shaw) We wish to retry downloading images because we have been seeing
   175  	// some flakey performance from the ubuntu cloud-images archive. This has lead to rare
   176  	// but disruptive failures to bootstrap due to these transient failures.
   177  	// Ideally this should be handled at lxd's end. However, image download is handled by
   178  	// the lxd server/agent, this needs to be handled by lxd.
   179  	// TODO(jack-s-shaw) Remove retries here once it's been implemented in lxd. See this bug:
   180  	// https://github.com/canonical/lxd/issues/12672
   181  	err := retry.Call(retry.CallArgs{
   182  		Clock:       s.clock,
   183  		Attempts:    3,
   184  		Delay:       15 * time.Second,
   185  		BackoffFunc: retry.DoubleDelay,
   186  		Stop:        ctx.Done(),
   187  		Func:        attemptDownload,
   188  		IsFatalError: func(err error) bool {
   189  			// unfortunately the LXD client currently does not
   190  			// provide a way to differentiate between errors
   191  			return !strings.HasPrefix(err.Error(), "Failed remote image download")
   192  		},
   193  		NotifyFunc: func(_ error, attempt int) {
   194  			if callback != nil {
   195  				_ = callback(status.Provisioning, fmt.Sprintf("Failed remote LXD image download. Retrying. Attempt number %d", attempt+1), nil)
   196  			}
   197  		},
   198  	})
   199  	if err != nil {
   200  		return errors.Trace(err)
   201  	}
   202  	opInfo, err := op.GetTarget()
   203  	if err != nil {
   204  		return errors.Trace(err)
   205  	}
   206  	if opInfo.StatusCode != api.Success {
   207  		return fmt.Errorf("image copy failed: %s", opInfo.Err)
   208  	}
   209  	return nil
   210  }
   211  
   212  // baseLocalAlias returns the alias to assign to images for the
   213  // specified series. The alias is juju-specific, to support the
   214  // user supplying a customised image (e.g. CentOS with cloud-init).
   215  func baseLocalAlias(base, arch string, virtType instance.VirtType) string {
   216  	// We use a different alias for VMs, so that we can distinguish between
   217  	// a VM image and a container image. We don't add anything to the alias
   218  	// for containers to keep backwards compatibility with older versions
   219  	// of the image aliases.
   220  	switch virtType {
   221  	case api.InstanceTypeVM:
   222  		return fmt.Sprintf("juju/%s/%s/vm", base, arch)
   223  	default:
   224  		return fmt.Sprintf("juju/%s/%s", base, arch)
   225  	}
   226  }
   227  
   228  // baseRemoteAliases returns the aliases to look for in remotes.
   229  func baseRemoteAliases(base jujubase.Base, arch string) ([]string, error) {
   230  	alias, err := constructBaseRemoteAlias(base, arch)
   231  	if err != nil {
   232  		return nil, errors.Trace(err)
   233  	}
   234  	return []string{
   235  		alias,
   236  	}, nil
   237  }
   238  
   239  func isCompatibleVirtType(virtType instance.VirtType, instanceType string) bool {
   240  	if instanceType == "" && (virtType == api.InstanceTypeAny || virtType == api.InstanceTypeContainer) {
   241  		return true
   242  	}
   243  	return string(virtType) == instanceType
   244  }
   245  
   246  func constructBaseRemoteAlias(base jujubase.Base, arch string) (string, error) {
   247  	seriesOS := jujuos.OSTypeForName(base.OS)
   248  	switch seriesOS {
   249  	case jujuos.Ubuntu:
   250  		return path.Join(base.Channel.Track, arch), nil
   251  	case jujuos.CentOS:
   252  		if arch == jujuarch.AMD64 {
   253  			switch base.Channel.Track {
   254  			case "7", "8":
   255  				return fmt.Sprintf("centos/%s/cloud/amd64", base.Channel.Track), nil
   256  			case "9":
   257  				return "centos/9-Stream/cloud/amd64", nil
   258  			}
   259  		}
   260  	}
   261  	return "", errors.NotSupportedf("base %q", base.DisplayString())
   262  }