github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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  	"fmt"
     8  	"path"
     9  
    10  	"github.com/juju/errors"
    11  	"github.com/juju/juju/core/status"
    12  	"github.com/juju/juju/environs"
    13  	jujuarch "github.com/juju/utils/arch"
    14  	jujuos "github.com/juju/utils/os"
    15  	jujuseries "github.com/juju/utils/series"
    16  	"github.com/lxc/lxd/client"
    17  	"github.com/lxc/lxd/shared/api"
    18  )
    19  
    20  // SourcedImage is the result of a successful image acquisition.
    21  // It includes the relevant data that located the image.
    22  type SourcedImage struct {
    23  	// Image is the actual image data that was located.
    24  	Image *api.Image
    25  	// LXDServer is the image server that supplied the image.
    26  	LXDServer lxd.ImageServer
    27  }
    28  
    29  // FindImage searches the input sources in supplied order, looking for an OS
    30  // image matching the supplied series and architecture.
    31  // If found, the image and the server from which it was acquired are returned.
    32  // If the server is remote the image will be cached by LXD when used to create
    33  // a container.
    34  // Supplying true for copyLocal will copy the image to the local cache.
    35  // Copied images will have the juju/series/arch alias added to them.
    36  // The callback argument is used to report copy progress.
    37  func (s *Server) FindImage(
    38  	series, arch string,
    39  	sources []ServerSpec,
    40  	copyLocal bool,
    41  	callback environs.StatusCallbackFunc,
    42  ) (SourcedImage, error) {
    43  	if callback != nil {
    44  		callback(status.Provisioning, "acquiring LXD image", nil)
    45  	}
    46  
    47  	// First we check if we have the image locally.
    48  	localAlias := seriesLocalAlias(series, arch)
    49  	var target string
    50  	entry, _, err := s.GetImageAlias(localAlias)
    51  	if entry != nil {
    52  		// We already have an image with the given alias, so just use that.
    53  		target = entry.Target
    54  		image, _, err := s.GetImage(target)
    55  		if err == nil {
    56  			logger.Debugf("Found image locally - %q %q", image.Filename, target)
    57  			return SourcedImage{
    58  				Image:     image,
    59  				LXDServer: s.ContainerServer,
    60  			}, nil
    61  		}
    62  	}
    63  
    64  	sourced := SourcedImage{}
    65  	lastErr := fmt.Errorf("no matching image found")
    66  
    67  	// We don't have an image locally with the juju-specific alias,
    68  	// so look in each of the provided remote sources for any of the aliases
    69  	// that might identify the image we want.
    70  	aliases, err := seriesRemoteAliases(series, arch)
    71  	if err != nil {
    72  		return sourced, errors.Trace(err)
    73  	}
    74  	for _, remote := range sources {
    75  		source, err := ConnectImageRemote(remote)
    76  		if err != nil {
    77  			logger.Infof("failed to connect to %q: %s", remote.Host, err)
    78  			lastErr = errors.Trace(err)
    79  			continue
    80  		}
    81  		for _, alias := range aliases {
    82  			if result, _, err := source.GetImageAlias(alias); err == nil && result != nil && result.Target != "" {
    83  				target = result.Target
    84  				break
    85  			}
    86  		}
    87  		if target != "" {
    88  			image, _, err := source.GetImage(target)
    89  			if err == nil {
    90  				logger.Debugf("Found image remotely - %q %q %q", remote.Name, image.Filename, target)
    91  				sourced.Image = image
    92  				sourced.LXDServer = source
    93  				break
    94  			} else {
    95  				lastErr = errors.Trace(err)
    96  			}
    97  		}
    98  	}
    99  
   100  	if sourced.Image == nil {
   101  		return sourced, lastErr
   102  	}
   103  
   104  	// If requested, copy the image to the local cache, adding the local alias.
   105  	if copyLocal {
   106  		if err := s.CopyRemoteImage(sourced, []string{localAlias}, callback); err != nil {
   107  			return sourced, errors.Trace(err)
   108  		}
   109  
   110  		// Now that we have the image cached locally, we indicate in the return
   111  		// that the source is local instead of the remote where we found it.
   112  		sourced.LXDServer = s.ContainerServer
   113  	}
   114  
   115  	return sourced, nil
   116  }
   117  
   118  // CopyRemoteImage accepts an image sourced from a remote server and copies it
   119  // to the local cache
   120  func (s *Server) CopyRemoteImage(
   121  	sourced SourcedImage, aliases []string, callback environs.StatusCallbackFunc,
   122  ) error {
   123  	logger.Debugf("Copying image from remote server")
   124  
   125  	newAliases := make([]api.ImageAlias, len(aliases))
   126  	for i, a := range aliases {
   127  		newAliases[i] = api.ImageAlias{Name: a}
   128  	}
   129  
   130  	req := &lxd.ImageCopyArgs{Aliases: newAliases}
   131  	op, err := s.CopyImage(sourced.LXDServer, *sourced.Image, req)
   132  	if err != nil {
   133  		return errors.Trace(err)
   134  	}
   135  
   136  	// Report progress via callback if supplied.
   137  	if callback != nil {
   138  		progress := func(op api.Operation) {
   139  			if op.Metadata == nil {
   140  				return
   141  			}
   142  			for _, key := range []string{"fs_progress", "download_progress"} {
   143  				if value, ok := op.Metadata[key]; ok {
   144  					callback(status.Provisioning, fmt.Sprintf("Retrieving image: %s", value.(string)), nil)
   145  					return
   146  				}
   147  			}
   148  		}
   149  		_, err = op.AddHandler(progress)
   150  		if err != nil {
   151  			return errors.Trace(err)
   152  		}
   153  	}
   154  
   155  	if err := op.Wait(); err != nil {
   156  		return errors.Trace(err)
   157  	}
   158  	opInfo, err := op.GetTarget()
   159  	if err != nil {
   160  		return errors.Trace(err)
   161  	}
   162  	if opInfo.StatusCode != api.Success {
   163  		return fmt.Errorf("image copy failed: %s", opInfo.Err)
   164  	}
   165  	return nil
   166  }
   167  
   168  // seriesLocalAlias returns the alias to assign to images for the
   169  // specified series. The alias is juju-specific, to support the
   170  // user supplying a customised image (e.g. CentOS with cloud-init).
   171  func seriesLocalAlias(series, arch string) string {
   172  	return fmt.Sprintf("juju/%s/%s", series, arch)
   173  }
   174  
   175  // seriesRemoteAliases returns the aliases to look for in remotes.
   176  func seriesRemoteAliases(series, arch string) ([]string, error) {
   177  	seriesOS, err := jujuseries.GetOSFromSeries(series)
   178  	if err != nil {
   179  		return nil, errors.Trace(err)
   180  	}
   181  	switch seriesOS {
   182  	case jujuos.Ubuntu:
   183  		return []string{path.Join(series, arch)}, nil
   184  	case jujuos.CentOS:
   185  		if series == "centos7" && arch == jujuarch.AMD64 {
   186  			return []string{"centos/7/amd64"}, nil
   187  		}
   188  	case jujuos.OpenSUSE:
   189  		if series == "opensuseleap" && arch == jujuarch.AMD64 {
   190  			return []string{"opensuse/42.2/amd64"}, nil
   191  		}
   192  	}
   193  	return nil, errors.NotSupportedf("series %q", series)
   194  }