github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/oci/images.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package oci 5 6 import ( 7 "context" 8 "fmt" 9 "sort" 10 "strconv" 11 "strings" 12 "sync" 13 "time" 14 15 "github.com/juju/errors" 16 // jujuos "github.com/juju/utils/os" 17 "github.com/juju/utils/series" 18 19 "github.com/juju/juju/environs/imagemetadata" 20 "github.com/juju/juju/environs/instances" 21 "github.com/juju/juju/provider/oci/common" 22 23 ociCore "github.com/oracle/oci-go-sdk/core" 24 ) 25 26 const ( 27 BareMetal InstanceType = "metal" 28 VirtualMachine InstanceType = "vm" 29 GPUMachine InstanceType = "gpu" 30 31 // ImageTypeVM should be run on a virtual instance 32 ImageTypeVM ImageType = "vm" 33 // ImageTypeBM should be run on bare metal 34 ImageTypeBM ImageType = "metal" 35 // ImageTypeGPU should be run on an instance with attached GPUs 36 ImageTypeGPU ImageType = "gpu" 37 // ImageTypeGeneric should work on any type of instance (bare metal or virtual) 38 ImageTypeGeneric ImageType = "generic" 39 40 windowsOS = "Windows" 41 centOS = "CentOS" 42 ubuntuOS = "Canonical Ubuntu" 43 44 staleImageCacheTimeoutInMinutes = 30 45 ) 46 47 var globalImageCache = &ImageCache{} 48 var cacheMutex = &sync.Mutex{} 49 50 type InstanceType string 51 type ImageType string 52 53 type ImageVersion struct { 54 TimeStamp time.Time 55 Revision int 56 } 57 58 func setImageCache(cache *ImageCache) { 59 cacheMutex.Lock() 60 defer cacheMutex.Unlock() 61 62 globalImageCache = cache 63 } 64 65 func NewImageVersion(img ociCore.Image) (ImageVersion, error) { 66 var imgVersion ImageVersion 67 if img.DisplayName == nil { 68 return imgVersion, errors.Errorf("image does not have a display name") 69 } 70 fields := strings.Split(*img.DisplayName, "-") 71 if len(fields) < 2 { 72 return imgVersion, errors.Errorf("invalid image display name %q", *img.DisplayName) 73 } 74 timeStamp, err := time.Parse("2006.01.02", fields[len(fields)-2]) 75 if err != nil { 76 return imgVersion, errors.Annotatef(err, "parsing time for %q", *img.DisplayName) 77 } 78 79 revision, err := strconv.Atoi(fields[len(fields)-1]) 80 81 if err != nil { 82 return imgVersion, errors.Annotatef(err, "parsing revision for %q", *img.DisplayName) 83 } 84 85 imgVersion.TimeStamp = timeStamp 86 imgVersion.Revision = revision 87 return imgVersion, nil 88 } 89 90 // InstanceImage aggregates information pertinent to provider supplied 91 // images (eg: shapes it ca run on, type of instance it can run on, etc) 92 type InstanceImage struct { 93 // ImageType determines which type of image this is. Valid values are: 94 // vm, baremetal and generic 95 ImageType ImageType 96 // Id is the provider ID of the image 97 Id string 98 // Series is the series as known by juju 99 Series string 100 // Version is the version of the image 101 Version ImageVersion 102 // Raw stores the core.Image object 103 Raw ociCore.Image 104 105 // CompartmentId is the compartment Id where this image is available 106 CompartmentId *string 107 108 // InstanceTypes holds a list of shapes compatible with this image 109 InstanceTypes []instances.InstanceType 110 } 111 112 func (i *InstanceImage) SetInstanceTypes(types []instances.InstanceType) { 113 i.InstanceTypes = types 114 } 115 116 // byVersion sorts shapes by version number 117 type byVersion []InstanceImage 118 119 func (t byVersion) Len() int { 120 return len(t) 121 } 122 123 func (t byVersion) Swap(i, j int) { 124 t[i], t[j] = t[j], t[i] 125 } 126 127 func (t byVersion) Less(i, j int) bool { 128 // Sort in reverse order. Newer versions first in array 129 if t[i].Version.TimeStamp.Before(t[j].Version.TimeStamp) { 130 return false 131 } 132 if t[i].Version.TimeStamp.Equal(t[j].Version.TimeStamp) { 133 if t[i].Version.Revision < t[j].Version.Revision { 134 return false 135 } 136 } 137 return true 138 } 139 140 // ImageCache holds a cache of all provider images for a fixed 141 // amount of time before it becomes stale 142 type ImageCache struct { 143 // images []InstanceImage 144 images map[string][]InstanceImage 145 146 // shapeToInstanceImageMap map[string][]InstanceImage 147 148 lastRefresh time.Time 149 } 150 151 func (i *ImageCache) ImageMap() map[string][]InstanceImage { 152 return i.images 153 } 154 155 // SetLastRefresh sets the lastRefresh attribute of ImageCache 156 // This is used mostly for testing purposes 157 func (i *ImageCache) SetLastRefresh(t time.Time) { 158 i.lastRefresh = t 159 } 160 161 func (i *ImageCache) SetImages(images map[string][]InstanceImage) { 162 i.images = images 163 } 164 165 func (i *ImageCache) isStale() bool { 166 threshold := i.lastRefresh.Add(staleImageCacheTimeoutInMinutes * time.Minute) 167 now := time.Now() 168 if now.After(threshold) { 169 return true 170 } 171 return false 172 } 173 174 // ImageMetadata returns an array of imagemetadata.ImageMetadata for 175 // all images that are currently in cache, matching the provided series 176 // If defaultVirtType is specified, all generic images will inherit the 177 // value of defaultVirtType. 178 func (i ImageCache) ImageMetadata(series string, defaultVirtType string) []*imagemetadata.ImageMetadata { 179 var metadata []*imagemetadata.ImageMetadata 180 181 images, ok := i.images[series] 182 if !ok { 183 return metadata 184 } 185 for _, val := range images { 186 if val.ImageType == ImageTypeGeneric { 187 if defaultVirtType != "" { 188 val.ImageType = ImageType(defaultVirtType) 189 } else { 190 val.ImageType = ImageTypeVM 191 } 192 } 193 imgMeta := &imagemetadata.ImageMetadata{ 194 Id: val.Id, 195 Arch: "amd64", 196 // TODO(gsamfira): add region name? 197 // RegionName: region, 198 Version: val.Series, 199 VirtType: string(val.ImageType), 200 } 201 metadata = append(metadata, imgMeta) 202 } 203 204 return metadata 205 } 206 207 // SupportedShapes returns the InstanceTypes available for images matching 208 // the supplied series 209 func (i ImageCache) SupportedShapes(series string) []instances.InstanceType { 210 matches := map[string]int{} 211 ret := []instances.InstanceType{} 212 // TODO(gsamfira): Find a better way for this. 213 images, ok := i.images[series] 214 if !ok { 215 return ret 216 } 217 for _, img := range images { 218 for _, instType := range img.InstanceTypes { 219 if _, ok := matches[instType.Name]; !ok { 220 matches[instType.Name] = 1 221 ret = append(ret, instType) 222 } 223 } 224 } 225 return ret 226 } 227 228 func getImageType(img ociCore.Image) ImageType { 229 if img.DisplayName == nil { 230 return ImageTypeGeneric 231 } 232 name := strings.ToLower(*img.DisplayName) 233 if strings.Contains(name, "-vm-") { 234 return ImageTypeVM 235 } 236 if strings.Contains(name, "-bm-") { 237 return ImageTypeBM 238 } 239 if strings.Contains(name, "-gpu-") { 240 return ImageTypeGPU 241 } 242 return ImageTypeGeneric 243 } 244 245 func getCentOSSeries(img ociCore.Image) (string, error) { 246 if img.OperatingSystemVersion == nil || *img.OperatingSystem != centOS { 247 return "", errors.NotSupportedf("invalid Operating system") 248 } 249 splitVersion := strings.Split(*img.OperatingSystemVersion, ".") 250 if len(splitVersion) < 1 { 251 return "", errors.NotSupportedf("invalid centOS version: %v", *img.OperatingSystemVersion) 252 } 253 tmpVersion := fmt.Sprintf("%s%s", strings.ToLower(*img.OperatingSystem), splitVersion[0]) 254 255 // call series.CentOSVersionSeries to validate that the version 256 // of CentOS is supported by juju 257 logger.Tracef("Determining CentOS series for: %s", tmpVersion) 258 return series.CentOSVersionSeries(tmpVersion) 259 } 260 261 func NewInstanceImage(img ociCore.Image, compartmentID *string) (imgType InstanceImage, err error) { 262 var imgSeries string 263 switch osVersion := *img.OperatingSystem; osVersion { 264 case windowsOS: 265 tmp := fmt.Sprintf("%s %s", *img.OperatingSystem, *img.OperatingSystemVersion) 266 logger.Tracef("Determining Windows series for: %s", tmp) 267 imgSeries, err = series.WindowsVersionSeries(tmp) 268 case centOS: 269 imgSeries, err = getCentOSSeries(img) 270 case ubuntuOS: 271 logger.Tracef("Determining Ubuntu series for: %s", *img.OperatingSystemVersion) 272 imgSeries, err = series.VersionSeries(*img.OperatingSystemVersion) 273 default: 274 return imgType, errors.NotSupportedf("os %s", osVersion) 275 } 276 277 if err != nil { 278 return imgType, err 279 } 280 281 imgType.ImageType = getImageType(img) 282 imgType.Id = *img.Id 283 imgType.Series = imgSeries 284 imgType.Raw = img 285 imgType.CompartmentId = compartmentID 286 287 version, err := NewImageVersion(img) 288 if err != nil { 289 return imgType, err 290 } 291 imgType.Version = version 292 293 return imgType, nil 294 } 295 296 func instanceTypes(cli common.OCIComputeClient, compartmentID, imageID *string) ([]instances.InstanceType, error) { 297 if cli == nil { 298 return nil, errors.Errorf("cannot use nil client") 299 } 300 301 request := ociCore.ListShapesRequest{ 302 CompartmentId: compartmentID, 303 ImageId: imageID, 304 } 305 // fetch all shapes from the provider 306 shapes, err := cli.ListShapes(context.Background(), request) 307 if err != nil { 308 return nil, errors.Trace(err) 309 } 310 311 // convert shapes to InstanceType 312 arch := []string{"amd64"} 313 types := []instances.InstanceType{} 314 for _, val := range shapes.Items { 315 spec, ok := shapeSpecs[*val.Shape] 316 if !ok { 317 logger.Debugf("shape %s does not have a mapping", *val.Shape) 318 continue 319 } 320 instanceType := string(spec.Type) 321 newType := instances.InstanceType{ 322 Name: *val.Shape, 323 Arches: arch, 324 Mem: uint64(spec.Memory), 325 CpuCores: uint64(spec.Cpus), 326 // its not really virtualization type. We have just 3 types of images: 327 // bare metal, virtual and generic (works on metal and VM). 328 VirtType: &instanceType, 329 } 330 types = append(types, newType) 331 } 332 return types, nil 333 } 334 335 func refreshImageCache(cli common.OCIComputeClient, compartmentID *string) (*ImageCache, error) { 336 cacheMutex.Lock() 337 defer cacheMutex.Unlock() 338 339 if globalImageCache.isStale() == false { 340 return globalImageCache, nil 341 } 342 343 request := ociCore.ListImagesRequest{ 344 CompartmentId: compartmentID, 345 } 346 response, err := cli.ListImages(context.Background(), request) 347 if err != nil { 348 return nil, errors.Annotatef(err, "listing provider images") 349 } 350 351 images := map[string][]InstanceImage{} 352 353 for _, val := range response.Items { 354 instTypes, err := instanceTypes(cli, compartmentID, val.Id) 355 if err != nil { 356 return nil, err 357 } 358 img, err := NewInstanceImage(val, compartmentID) 359 if err != nil { 360 if val.Id != nil { 361 logger.Debugf("error parsing image %q: %q", *val.Id, err) 362 } else { 363 logger.Debugf("error parsing image %q", err) 364 } 365 continue 366 } 367 img.SetInstanceTypes(instTypes) 368 images[img.Series] = append(images[img.Series], img) 369 } 370 for v := range images { 371 sort.Sort(byVersion(images[v])) 372 } 373 globalImageCache = &ImageCache{ 374 images: images, 375 lastRefresh: time.Now(), 376 } 377 return globalImageCache, nil 378 } 379 380 // findInstanceSpec returns an *InstanceSpec, imagelist name 381 // satisfying the supplied instanceConstraint 382 func findInstanceSpec( 383 allImageMetadata []*imagemetadata.ImageMetadata, 384 instanceType []instances.InstanceType, 385 ic *instances.InstanceConstraint, 386 ) (*instances.InstanceSpec, string, error) { 387 388 logger.Debugf("received %d image(s): %v", len(allImageMetadata), allImageMetadata) 389 filtered := []*imagemetadata.ImageMetadata{} 390 // Filter by series. imgCache.supportedShapes() and 391 // imgCache.imageMetadata() will return filtered values 392 // by series already. This additional filtering is done 393 // in case someone wants to use this function with values 394 // not returned by the above two functions 395 for _, val := range allImageMetadata { 396 if val.Version != ic.Series { 397 continue 398 } 399 filtered = append(filtered, val) 400 } 401 402 images := instances.ImageMetadataToImages(filtered) 403 spec, err := instances.FindInstanceSpec(images, ic, instanceType) 404 if err != nil { 405 return nil, "", errors.Trace(err) 406 } 407 408 return spec, spec.Image.Id, nil 409 }