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 }