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  }