github.com/solo-io/unik@v0.0.0-20190717152701-a58d3e8e33b7/pkg/providers/openstack/stage.go (about)

     1  package openstack
     2  
     3  import (
     4  	"fmt"
     5  	"github.com/sirupsen/logrus"
     6  	"github.com/emc-advanced-dev/pkg/errors"
     7  	unikos "github.com/solo-io/unik/pkg/os"
     8  	"github.com/solo-io/unik/pkg/types"
     9  	"github.com/rackspace/gophercloud"
    10  	"github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
    11  	"github.com/rackspace/gophercloud/openstack/imageservice/v2/images"
    12  	"github.com/rackspace/gophercloud/pagination"
    13  	"math"
    14  	"os"
    15  	"time"
    16  )
    17  
    18  func (p *OpenstackProvider) Stage(params types.StageImageParams) (_ *types.Image, err error) {
    19  	imageList, err := p.ListImages()
    20  	if err != nil {
    21  		return nil, errors.New("failed to retrieve image list", err)
    22  	}
    23  
    24  	// Handle image name collision.
    25  	for _, image := range imageList {
    26  		if image.Name == params.Name {
    27  			if !params.Force {
    28  				return nil, errors.New(fmt.Sprintf("an image already exists with name '%s', try again with --force", params.Name), nil)
    29  			} else {
    30  				logrus.WithField("image", image).Warnf("force: deleting previous image with name '%s'", params.Name)
    31  				err = p.DeleteImage(image.Id, true)
    32  				if err != nil {
    33  					return nil, errors.New("failed to remove existing image", err)
    34  				}
    35  			}
    36  		}
    37  	}
    38  
    39  	clientGlance, err := p.newClientGlance()
    40  	if err != nil {
    41  		return nil, errors.New("creating new glance client session", err)
    42  	}
    43  	clientNova, err := p.newClientNova()
    44  	if err != nil {
    45  		return nil, errors.New("creating new nova client session", err)
    46  	}
    47  
    48  	logrus.WithFields(logrus.Fields{
    49  		"params": params,
    50  	}).Info("creating boot image from raw image")
    51  
    52  	rawImageFile, err := os.Stat(params.RawImage.LocalImagePath)
    53  	if err != nil {
    54  		return nil, errors.New("statting raw image file", err)
    55  	}
    56  
    57  	// TODO: Obtain image LOGICAL size, not actual (e.g. 10GB for OSv, not 8MB)
    58  	imageSizeB := rawImageFile.Size()
    59  	imageSizeMB := int(unikos.Bytes(imageSizeB).ToMegaBytes())
    60  
    61  	// Pick flavor.
    62  	flavor, err := pickFlavor(clientNova, imageSizeMB, 0)
    63  	if err != nil {
    64  		return nil, errors.New("picking a flavor", err)
    65  	}
    66  
    67  	logrus.WithFields(logrus.Fields{
    68  		"imageSizeB":  imageSizeB,
    69  		"imageSizeMB": imageSizeMB,
    70  		"flavor":      flavor,
    71  	}).Debug("pushing image to openstack")
    72  
    73  	// Push image to OpenStack.
    74  	createdImage, err := pushImage(clientGlance, params.Name, params.RawImage.LocalImagePath, flavor)
    75  	if err != nil {
    76  		return nil, errors.New("pushing image", err)
    77  	}
    78  
    79  	image := &types.Image{
    80  		Id:             createdImage.ID,
    81  		Name:           createdImage.Name,
    82  		RunSpec:        params.RawImage.RunSpec,
    83  		StageSpec:      params.RawImage.StageSpec,
    84  		SizeMb:         int64(imageSizeMB),
    85  		Infrastructure: types.Infrastructure_OPENSTACK,
    86  		Created:        time.Now(),
    87  	}
    88  
    89  	// Update state.
    90  	if err := p.state.ModifyImages(func(images map[string]*types.Image) error {
    91  		images[createdImage.ID] = image
    92  		return nil
    93  	}); err != nil {
    94  		return nil, errors.New("failed to modify image map in state", err)
    95  	}
    96  
    97  	logrus.WithFields(logrus.Fields{"image": image}).Infof("image created succesfully")
    98  	return image, nil
    99  }
   100  
   101  // pickFlavor picks flavor that best matches criteria (i.e. HDD size and RAM size).
   102  // While diskMB is required, memoryMB is optional (set to -1 to ignore).
   103  func pickFlavor(clientNova *gophercloud.ServiceClient, diskMB int, memoryMB int) (*flavors.Flavor, error) {
   104  	if diskMB <= 0 {
   105  		return nil, errors.New("Please specify disk size.", nil)
   106  	}
   107  
   108  	flavs, err := listFlavors(clientNova, int(math.Ceil(float64(diskMB)/1024)), memoryMB)
   109  	if err != nil {
   110  		return nil, errors.New("listing flavors", err)
   111  	}
   112  
   113  	// Find smallest flavor for given conditions.
   114  	logrus.WithField("flavors", flavs).Infof("Find smallest flavor for conditions: diskMB >= %d AND memoryMB >= %d\n", diskMB, memoryMB)
   115  
   116  	var bestFlavor flavors.Flavor
   117  	var minDiffDisk int = -1
   118  	var minDiffMem int = -1
   119  	for _, f := range flavs {
   120  		diffDisk := f.Disk*1024 - diskMB
   121  		var diffMem int = 0 // 0 is best value
   122  		if memoryMB > 0 {
   123  			diffMem = f.RAM - memoryMB
   124  		}
   125  
   126  		if diffDisk >= 0 && // disk is big enough
   127  			(minDiffDisk == -1 || minDiffDisk > diffDisk) && // disk is smaller than current best, but still big enough
   128  			diffMem >= 0 && // memory is big enough
   129  			(minDiffMem == -1 || minDiffMem >= diffMem) { // memory is smaller than current best, but still big enough
   130  			bestFlavor, minDiffDisk, minDiffMem = f, diffDisk, diffMem
   131  		}
   132  	}
   133  	if minDiffDisk == -1 {
   134  		return nil, errors.New(fmt.Sprintf("No flavor fits required conditions: diskMB >= %d AND memoryMB >= %d\n", diskMB, memoryMB), nil)
   135  	}
   136  	return &bestFlavor, nil
   137  }
   138  
   139  // listFlavors returns list of all flavors.
   140  func listFlavors(clientNova *gophercloud.ServiceClient, minDiskGB int, minMemoryMB int) ([]flavors.Flavor, error) {
   141  	var flavs []flavors.Flavor = make([]flavors.Flavor, 0)
   142  
   143  	pagerFlavors := flavors.ListDetail(clientNova, flavors.ListOpts{
   144  		MinDisk: minDiskGB,
   145  		MinRAM:  minMemoryMB,
   146  	})
   147  	if err := pagerFlavors.EachPage(func(page pagination.Page) (bool, error) {
   148  		flavorList, err := flavors.ExtractFlavors(page)
   149  		if err != nil {
   150  			return false, errors.New(fmt.Sprintf("reading flavors from %+v", page), err)
   151  		}
   152  		for _, f := range flavorList {
   153  			flavs = append(flavs, f)
   154  		}
   155  		return true, nil
   156  	}); err != nil {
   157  		return nil, errors.New("reading flavors from pages", err)
   158  	}
   159  	return flavs, nil
   160  }
   161  
   162  // pushImage first creates meta for image at OpenStack, then it sends binary data for it, the qcow2 image.
   163  func pushImage(clientGlance *gophercloud.ServiceClient, imageName string, imageFilepath string, flavor *flavors.Flavor) (*images.Image, error) {
   164  	// Create metadata (on OpenStack).
   165  	createdImage, err := createImage(clientGlance, imageName, flavor)
   166  	if err != nil {
   167  		return nil, errors.New("creating openstack image metadata", err)
   168  	}
   169  
   170  	// Send the image binary data to OpenStack
   171  	if err = uploadImage(clientGlance, createdImage.ID, imageFilepath); err != nil {
   172  		return nil, errors.New("uploading image", err)
   173  	}
   174  
   175  	return createdImage, nil
   176  }
   177  
   178  // createImage creates image metadata on OpenStack.
   179  func createImage(clientGlance *gophercloud.ServiceClient, name string, flavor *flavors.Flavor) (*images.Image, error) {
   180  	createdImage, err := images.Create(clientGlance, images.CreateOpts{
   181  		Name:             name,
   182  		DiskFormat:       "qcow2",
   183  		ContainerFormat:  "bare",
   184  		MinDiskGigabytes: flavor.Disk,
   185  	}).Extract()
   186  	if err != nil {
   187  		return nil, errors.New("creating image", err)
   188  	}
   189  	logrus.WithFields(logrus.Fields{
   190  		"createdImage": createdImage,
   191  	}).Info("Created image")
   192  	return createdImage, nil
   193  }
   194  
   195  // uploadImage uploads image binary data to existing OpenStack image metadata.
   196  func uploadImage(clientGlance *gophercloud.ServiceClient, imageId string, filepath string) error {
   197  	logrus.WithFields(logrus.Fields{
   198  		"filepath": filepath,
   199  	}).Info("Uploading composed image to OpenStack")
   200  
   201  	f, err := os.Open(filepath)
   202  	if err != nil {
   203  		return errors.New("opening file", err)
   204  	}
   205  	defer f.Close()
   206  
   207  	res := images.Upload(clientGlance, imageId, f)
   208  	if res.Err != nil {
   209  		return errors.New("uploading image api call", res.Err)
   210  	}
   211  	return nil
   212  }