github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/tools/lxdclient/client_image.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // +build go1.3
     5  
     6  package lxdclient
     7  
     8  import (
     9  	"fmt"
    10  
    11  	"github.com/juju/errors"
    12  	"github.com/juju/loggo"
    13  	"github.com/lxc/lxd"
    14  
    15  	"github.com/juju/juju/utils/stringforwarder"
    16  )
    17  
    18  type rawImageClient interface {
    19  	GetAlias(string) string
    20  }
    21  
    22  type remoteClient interface {
    23  	URL() string
    24  	GetAlias(name string) string
    25  	// This is like lxd.Client.CopyImage() but simplified and allows us to
    26  	// inject a testing double.
    27  	CopyImage(imageTarget string, dest rawImageClient, aliases []string, callback func(string)) error
    28  }
    29  
    30  type imageClient struct {
    31  	raw             rawImageClient
    32  	connectToSource func(Remote) (remoteClient, error)
    33  }
    34  
    35  type rawWrapper struct {
    36  	*lxd.Client
    37  }
    38  
    39  func (r rawWrapper) URL() string {
    40  	return r.Client.BaseURL
    41  }
    42  
    43  func (r rawWrapper) CopyImage(imageTarget string, dest rawImageClient, aliases []string, callback func(string)) error {
    44  	rawDest, ok := dest.(*lxd.Client)
    45  	if !ok {
    46  		return errors.Errorf("can only copy images to a real lxd.Client instance")
    47  	}
    48  	return r.Client.CopyImage(
    49  		imageTarget,
    50  		rawDest,
    51  		false,   // copy_aliases
    52  		aliases, // create these aliases
    53  		false,   // make the image public
    54  		true,    // autoUpdate,
    55  		callback,
    56  	)
    57  }
    58  
    59  func connectToRaw(remote Remote) (remoteClient, error) {
    60  	raw, err := newRawClient(remote)
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  	return rawWrapper{raw}, nil
    65  }
    66  
    67  // progressContext takes progress messages from LXD and just writes them to
    68  // the associated logger at the given log level.
    69  type progressContext struct {
    70  	logger  loggo.Logger
    71  	level   loggo.Level
    72  	context string       // a format string that should take a single %s parameter
    73  	forward func(string) // pass messages onward
    74  }
    75  
    76  func (p *progressContext) copyProgress(progress string) {
    77  	msg := fmt.Sprintf(p.context, progress)
    78  	p.logger.Logf(p.level, msg)
    79  	if p.forward != nil {
    80  		p.forward(msg)
    81  	}
    82  }
    83  
    84  // EnsureImageExists makes sure we have a local image so we can launch a
    85  // container.
    86  // @param series: OS series (trusty, precise, etc)
    87  // @param architecture: (TODO) The architecture of the image we want to use
    88  // @param trustLocal: (TODO) check if we already have an image with the right alias.
    89  // Setting this to False means we will always check the remote sources and only
    90  // launch the newest version.
    91  // @param sources: a list of Remotes that we will look in for the image.
    92  // @param copyProgressHandler: a callback function. If we have to download an
    93  // image, we will call this with messages indicating how much of the download
    94  // we have completed (and where we are downloading it from).
    95  func (i *imageClient) EnsureImageExists(series string, sources []Remote, copyProgressHandler func(string)) error {
    96  	// TODO(jam) Find a way to test this, even though lxd.Client can't
    97  	// really be stubbed out because CopyImage takes one directly and pokes
    98  	// at private methods so we can't easily tweak it.
    99  	name := i.ImageNameForSeries(series)
   100  
   101  	// TODO(jam) Add a flag to not trust local aliases, which would allow
   102  	// non-state machines to only trust the alias that is set on the state
   103  	// machines.
   104  	// if IgnoreLocalAliases {}
   105  	target := i.raw.GetAlias(name)
   106  	if target != "" {
   107  		// GetAlias returns "" if the alias is not found, else it
   108  		// returns the Target of the alias (the hash)
   109  		return nil
   110  	}
   111  
   112  	var lastErr error
   113  	for _, remote := range sources {
   114  		source, err := i.connectToSource(remote)
   115  		if err != nil {
   116  			logger.Infof("failed to connect to %q: %s", remote.Host, err)
   117  			lastErr = err
   118  			continue
   119  		}
   120  
   121  		// TODO(jam): there are multiple possible spellings for aliases,
   122  		// unfortunately. cloud-images only hosts ubuntu images, and
   123  		// aliases them as "trusty" or "trusty/amd64" or
   124  		// "trusty/amd64/20160304". However, we should be more
   125  		// explicit. and use "ubuntu/trusty/amd64" as our default
   126  		// naming scheme, and only fall back for synchronization.
   127  		target := source.GetAlias(series)
   128  		if target == "" {
   129  			logger.Infof("no image for %s found in %s", name, source.URL())
   130  			// TODO(jam) Add a test that we skip sources that don't
   131  			// have what we are looking for
   132  			continue
   133  		}
   134  		logger.Infof("found image from %s for %s = %s",
   135  			source.URL(), series, target)
   136  		forwarder := stringforwarder.New(copyProgressHandler)
   137  		defer func() {
   138  			dropCount := forwarder.Stop()
   139  			logger.Debugf("dropped %d progress messages", dropCount)
   140  		}()
   141  		adapter := &progressContext{
   142  			logger:  logger,
   143  			level:   loggo.INFO,
   144  			context: fmt.Sprintf("copying image for %s from %s: %%s", name, source.URL()),
   145  			forward: forwarder.Forward,
   146  		}
   147  		err = source.CopyImage(target, i.raw, []string{name}, adapter.copyProgress)
   148  		if err != nil {
   149  			// TODO(jam) Should this be fatal? Or just set lastErr
   150  			// and then continue on?
   151  			logger.Warningf("error copying image: %s", err)
   152  			return errors.Annotatef(err, "unable to get LXD image for %s", name)
   153  		}
   154  		return nil
   155  	}
   156  	return lastErr
   157  }
   158  
   159  // A common place to compute image names (aliases) based on the series
   160  func (i imageClient) ImageNameForSeries(series string) string {
   161  	// TODO(jam) Do we need 'ubuntu' in there? We only need it if "series"
   162  	// would collide, but all our supported series are disjoint
   163  	return fmt.Sprintf("ubuntu-%s", series)
   164  }