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 }