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 }