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