github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/provider/vsphere/ova_import_manager.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 // +build !gccgo 5 6 package vsphere 7 8 import ( 9 "archive/tar" 10 "io" 11 "io/ioutil" 12 "net/http" 13 "net/url" 14 "os" 15 "path/filepath" 16 17 "github.com/juju/errors" 18 "github.com/juju/govmomi/object" 19 "github.com/juju/govmomi/vim25/progress" 20 "github.com/juju/govmomi/vim25/soap" 21 "github.com/juju/govmomi/vim25/types" 22 "golang.org/x/net/context" 23 ) 24 25 /* 26 This file contains implementation of the process of importing OVF template using vsphere API. This process can be splitted in the following steps 27 1. Download OVA template 28 2. Extract it to a temp folder and load ovf file from it. 29 3. Call CreateImportSpec method from vsphere API https://www.vmware.com/support/developer/vc-sdk/visdk41pubs/ApiReference/. This method validates the OVF descriptor against the hardware supported by the host system. If the validation succeeds, return a result containing: 30 * An ImportSpec to use when importing the entity. 31 * A list of items to upload (for example disk backing files, ISO images etc.) 32 4. Prepare all necessary parameters (CPU, mem, etc.) and call ImportVApp method https://www.vmware.com/support/developer/vc-sdk/visdk41pubs/ApiReference/. This method is responsible for actually creating VM. This method return HttpNfcLease (https://www.vmware.com/support/developer/vc-sdk/visdk41pubs/ApiReference/vim.HttpNfcLease.html) object, that is used to monitor status of the process. 33 5. Upload virtual disk contents (that usually consist of a single vmdk file) 34 6. Call HttpNfcLeaseComplete https://www.vmware.com/support/developer/vc-sdk/visdk41pubs/ApiReference/ and indicate that the process of uploading is finished - this step finishes the process. 35 */ 36 37 //this type implements progress.Sinker interface, that is requred to obtain the status of uploading an item to vspehere 38 type ovaFileItem struct { 39 url *url.URL 40 item types.OvfFileItem 41 ch chan progress.Report 42 } 43 44 func (o ovaFileItem) Sink() chan<- progress.Report { 45 return o.ch 46 } 47 48 type ovaImportManager struct { 49 client *client 50 } 51 52 func (m *ovaImportManager) importOva(ecfg *environConfig, instSpec *instanceSpec) (*object.VirtualMachine, error) { 53 folders, err := m.client.datacenter.Folders(context.TODO()) 54 if err != nil { 55 return nil, errors.Trace(err) 56 } 57 58 basePath, err := ioutil.TempDir("", "") 59 if err != nil { 60 return nil, errors.Trace(err) 61 } 62 defer func() { 63 if err := os.RemoveAll(basePath); err != nil { 64 logger.Errorf("can't remove temp directory, error: %s", err.Error()) 65 } 66 }() 67 ovf, err := m.downloadOva(basePath, instSpec.img.URL) 68 if err != nil { 69 return nil, errors.Trace(err) 70 } 71 72 cisp := types.OvfCreateImportSpecParams{ 73 EntityName: instSpec.machineID, 74 PropertyMapping: []types.KeyValue{ 75 types.KeyValue{Key: "public-keys", Value: instSpec.sshKey}, 76 types.KeyValue{Key: "user-data", Value: string(instSpec.userData)}, 77 }, 78 } 79 80 ovfManager := object.NewOvfManager(m.client.connection.Client) 81 resourcePool := object.NewReference(m.client.connection.Client, *instSpec.zone.r.ResourcePool) 82 datastore := object.NewReference(m.client.connection.Client, instSpec.zone.r.Datastore[0]) 83 spec, err := ovfManager.CreateImportSpec(context.TODO(), string(ovf), resourcePool, datastore, cisp) 84 if err != nil { 85 return nil, errors.Trace(err) 86 } 87 88 if spec.Error != nil { 89 return nil, errors.New(spec.Error[0].LocalizedMessage) 90 } 91 s := &spec.ImportSpec.(*types.VirtualMachineImportSpec).ConfigSpec 92 s.NumCPUs = int(*instSpec.hwc.CpuCores) 93 s.MemoryMB = int64(*instSpec.hwc.Mem) 94 s.CpuAllocation = &types.ResourceAllocationInfo{ 95 Limit: int64(*instSpec.hwc.CpuPower), 96 Reservation: int64(*instSpec.hwc.CpuPower), 97 } 98 s.ExtraConfig = append(s.ExtraConfig, &types.OptionValue{ 99 Key: metadataKeyControllerUUID, Value: instSpec.controllerUUID, 100 }) 101 if instSpec.isController { 102 s.ExtraConfig = append(s.ExtraConfig, &types.OptionValue{ 103 Key: metadataKeyIsController, Value: metadataValueIsController, 104 }) 105 } 106 for _, d := range s.DeviceChange { 107 if disk, ok := d.GetVirtualDeviceConfigSpec().Device.(*types.VirtualDisk); ok { 108 if disk.CapacityInKB < int64(*instSpec.hwc.RootDisk*1024) { 109 disk.CapacityInKB = int64(*instSpec.hwc.RootDisk * 1024) 110 } 111 //Set UnitNumber to -1 if it is unset in ovf file template (in this case it is parces as 0) 112 //but 0 causes an error for disk devices 113 if disk.UnitNumber == 0 { 114 disk.UnitNumber = -1 115 } 116 } 117 } 118 if ecfg.externalNetwork() != "" { 119 s.DeviceChange = append(s.DeviceChange, &types.VirtualDeviceConfigSpec{ 120 Operation: types.VirtualDeviceConfigSpecOperationAdd, 121 Device: &types.VirtualE1000{ 122 VirtualEthernetCard: types.VirtualEthernetCard{ 123 VirtualDevice: types.VirtualDevice{ 124 Backing: &types.VirtualEthernetCardNetworkBackingInfo{ 125 VirtualDeviceDeviceBackingInfo: types.VirtualDeviceDeviceBackingInfo{ 126 DeviceName: ecfg.externalNetwork(), 127 }, 128 }, 129 Connectable: &types.VirtualDeviceConnectInfo{ 130 StartConnected: true, 131 AllowGuestControl: true, 132 }, 133 }, 134 }, 135 }, 136 }) 137 } 138 rp := object.NewResourcePool(m.client.connection.Client, *instSpec.zone.r.ResourcePool) 139 lease, err := rp.ImportVApp(context.TODO(), spec.ImportSpec, folders.VmFolder, nil) 140 if err != nil { 141 return nil, errors.Annotatef(err, "failed to import vapp") 142 } 143 144 info, err := lease.Wait(context.TODO()) 145 if err != nil { 146 return nil, errors.Trace(err) 147 } 148 items := []ovaFileItem{} 149 for _, device := range info.DeviceUrl { 150 for _, item := range spec.FileItem { 151 if device.ImportKey != item.DeviceId { 152 continue 153 } 154 155 u, err := m.client.connection.Client.ParseURL(device.Url) 156 if err != nil { 157 return nil, errors.Trace(err) 158 } 159 160 i := ovaFileItem{ 161 url: u, 162 item: item, 163 ch: make(chan progress.Report), 164 } 165 items = append(items, i) 166 } 167 } 168 169 for _, i := range items { 170 err = m.uploadImage(i, basePath) 171 if err != nil { 172 return nil, errors.Trace(err) 173 } 174 } 175 lease.HttpNfcLeaseComplete(context.TODO()) 176 return object.NewVirtualMachine(m.client.connection.Client, info.Entity), nil 177 } 178 179 func (m *ovaImportManager) downloadOva(basePath, url string) (string, error) { 180 logger.Debugf("Downloading ova file from url: %s", url) 181 resp, err := http.Get(url) 182 if err != nil { 183 return "", errors.Trace(err) 184 } 185 defer resp.Body.Close() 186 if resp.StatusCode != http.StatusOK { 187 return "", errors.Errorf("can't download ova file from url: %s, status: %d", url, resp.StatusCode) 188 } 189 190 ovfFilePath, err := m.extractOva(basePath, resp.Body) 191 if err != nil { 192 return "", errors.Trace(err) 193 } 194 file, err := os.Open(ovfFilePath) 195 defer file.Close() 196 if err != nil { 197 return "", errors.Trace(err) 198 } 199 bytes, err := ioutil.ReadAll(file) 200 if err != nil { 201 return "", errors.Trace(err) 202 } 203 return string(bytes), nil 204 } 205 206 func (m *ovaImportManager) extractOva(basePath string, body io.Reader) (string, error) { 207 logger.Debugf("Extracting ova to path: %s", basePath) 208 tarBallReader := tar.NewReader(body) 209 var ovfFileName string 210 211 for { 212 header, err := tarBallReader.Next() 213 if err != nil { 214 if err == io.EOF { 215 break 216 } 217 return "", errors.Trace(err) 218 } 219 filename := header.Name 220 if filepath.Ext(filename) == ".ovf" { 221 ovfFileName = filename 222 } 223 logger.Debugf("Writing file %s", filename) 224 err = func() error { 225 writer, err := os.Create(filepath.Join(basePath, filename)) 226 defer writer.Close() 227 if err != nil { 228 return errors.Trace(err) 229 } 230 _, err = io.Copy(writer, tarBallReader) 231 if err != nil { 232 return errors.Trace(err) 233 } 234 return nil 235 }() 236 if err != nil { 237 return "", errors.Trace(err) 238 } 239 } 240 if ovfFileName == "" { 241 return "", errors.Errorf("no ovf file found in the archive") 242 } 243 logger.Debugf("Ova extracted successfully") 244 return filepath.Join(basePath, ovfFileName), nil 245 } 246 247 func (m *ovaImportManager) uploadImage(ofi ovaFileItem, basePath string) error { 248 filepath := filepath.Join(basePath, ofi.item.Path) 249 logger.Debugf("Uploading item from path: %s", filepath) 250 f, err := os.Open(filepath) 251 if err != nil { 252 return errors.Trace(err) 253 } 254 255 defer f.Close() 256 257 opts := soap.Upload{ 258 ContentLength: ofi.item.Size, 259 Progress: ofi, 260 } 261 262 opts.Method = "POST" 263 opts.Type = "application/x-vnd.vmware-streamVmdk" 264 logger.Debugf("Uploading image to %s", ofi.url) 265 go func() { 266 lastPercent := 0 267 for pr := <-ofi.ch; pr != nil; pr = <-ofi.ch { 268 curPercent := int(pr.Percentage()) 269 if curPercent-lastPercent >= 10 { 270 lastPercent = curPercent 271 logger.Debugf("Progress: %d%%", lastPercent) 272 } 273 } 274 }() 275 err = m.client.connection.Client.Upload(f, ofi.url, &opts) 276 if err == nil { 277 logger.Debugf("Image uploaded") 278 } 279 return errors.Trace(err) 280 }