github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/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 if instSpec.isState { 99 s.ExtraConfig = append(s.ExtraConfig, &types.OptionValue{Key: metadataKeyIsState, Value: metadataValueIsState}) 100 } 101 for _, d := range s.DeviceChange { 102 if disk, ok := d.GetVirtualDeviceConfigSpec().Device.(*types.VirtualDisk); ok { 103 if disk.CapacityInKB < int64(*instSpec.hwc.RootDisk*1024) { 104 disk.CapacityInKB = int64(*instSpec.hwc.RootDisk * 1024) 105 } 106 //Set UnitNumber to -1 if it is unset in ovf file template (in this case it is parces as 0) 107 //but 0 causes an error for disk devices 108 if disk.UnitNumber == 0 { 109 disk.UnitNumber = -1 110 } 111 } 112 } 113 if ecfg.externalNetwork() != "" { 114 s.DeviceChange = append(s.DeviceChange, &types.VirtualDeviceConfigSpec{ 115 Operation: types.VirtualDeviceConfigSpecOperationAdd, 116 Device: &types.VirtualE1000{ 117 VirtualEthernetCard: types.VirtualEthernetCard{ 118 VirtualDevice: types.VirtualDevice{ 119 Backing: &types.VirtualEthernetCardNetworkBackingInfo{ 120 VirtualDeviceDeviceBackingInfo: types.VirtualDeviceDeviceBackingInfo{ 121 DeviceName: ecfg.externalNetwork(), 122 }, 123 }, 124 Connectable: &types.VirtualDeviceConnectInfo{ 125 StartConnected: true, 126 AllowGuestControl: true, 127 }, 128 }, 129 }, 130 }, 131 }) 132 } 133 rp := object.NewResourcePool(m.client.connection.Client, *instSpec.zone.r.ResourcePool) 134 lease, err := rp.ImportVApp(context.TODO(), spec.ImportSpec, folders.VmFolder, nil) 135 if err != nil { 136 return nil, errors.Annotatef(err, "failed to import vapp") 137 } 138 139 info, err := lease.Wait(context.TODO()) 140 if err != nil { 141 return nil, errors.Trace(err) 142 } 143 items := []ovaFileItem{} 144 for _, device := range info.DeviceUrl { 145 for _, item := range spec.FileItem { 146 if device.ImportKey != item.DeviceId { 147 continue 148 } 149 150 u, err := m.client.connection.Client.ParseURL(device.Url) 151 if err != nil { 152 return nil, errors.Trace(err) 153 } 154 155 i := ovaFileItem{ 156 url: u, 157 item: item, 158 ch: make(chan progress.Report), 159 } 160 items = append(items, i) 161 } 162 } 163 164 for _, i := range items { 165 err = m.uploadImage(i, basePath) 166 if err != nil { 167 return nil, errors.Trace(err) 168 } 169 } 170 lease.HttpNfcLeaseComplete(context.TODO()) 171 return object.NewVirtualMachine(m.client.connection.Client, info.Entity), nil 172 } 173 174 func (m *ovaImportManager) downloadOva(basePath, url string) (string, error) { 175 logger.Debugf("Downloading ova file from url: %s", url) 176 resp, err := http.Get(url) 177 if err != nil { 178 return "", errors.Trace(err) 179 } 180 defer resp.Body.Close() 181 if resp.StatusCode != http.StatusOK { 182 return "", errors.Errorf("can't download ova file from url: %s, status: %d", url, resp.StatusCode) 183 } 184 185 ovfFilePath, err := m.extractOva(basePath, resp.Body) 186 if err != nil { 187 return "", errors.Trace(err) 188 } 189 file, err := os.Open(ovfFilePath) 190 defer file.Close() 191 if err != nil { 192 return "", errors.Trace(err) 193 } 194 bytes, err := ioutil.ReadAll(file) 195 if err != nil { 196 return "", errors.Trace(err) 197 } 198 return string(bytes), nil 199 } 200 201 func (m *ovaImportManager) extractOva(basePath string, body io.Reader) (string, error) { 202 logger.Debugf("Extracting ova to path: %s", basePath) 203 tarBallReader := tar.NewReader(body) 204 var ovfFileName string 205 206 for { 207 header, err := tarBallReader.Next() 208 if err != nil { 209 if err == io.EOF { 210 break 211 } 212 return "", errors.Trace(err) 213 } 214 filename := header.Name 215 if filepath.Ext(filename) == ".ovf" { 216 ovfFileName = filename 217 } 218 logger.Debugf("Writing file %s", filename) 219 err = func() error { 220 writer, err := os.Create(filepath.Join(basePath, filename)) 221 defer writer.Close() 222 if err != nil { 223 return errors.Trace(err) 224 } 225 _, err = io.Copy(writer, tarBallReader) 226 if err != nil { 227 return errors.Trace(err) 228 } 229 return nil 230 }() 231 if err != nil { 232 return "", errors.Trace(err) 233 } 234 } 235 if ovfFileName == "" { 236 return "", errors.Errorf("no ovf file found in the archive") 237 } 238 logger.Debugf("Ova extracted successfully") 239 return filepath.Join(basePath, ovfFileName), nil 240 } 241 242 func (m *ovaImportManager) uploadImage(ofi ovaFileItem, basePath string) error { 243 filepath := filepath.Join(basePath, ofi.item.Path) 244 logger.Debugf("Uploading item from path: %s", filepath) 245 f, err := os.Open(filepath) 246 if err != nil { 247 return errors.Trace(err) 248 } 249 250 defer f.Close() 251 252 opts := soap.Upload{ 253 ContentLength: ofi.item.Size, 254 Progress: ofi, 255 } 256 257 opts.Method = "POST" 258 opts.Type = "application/x-vnd.vmware-streamVmdk" 259 logger.Debugf("Uploading image to %s", ofi.url) 260 go func() { 261 lastPercent := 0 262 for pr := <-ofi.ch; pr != nil; pr = <-ofi.ch { 263 curPercent := int(pr.Percentage()) 264 if curPercent-lastPercent >= 10 { 265 lastPercent = curPercent 266 logger.Debugf("Progress: %d%%", lastPercent) 267 } 268 } 269 }() 270 err = m.client.connection.Client.Upload(f, ofi.url, &opts) 271 if err == nil { 272 logger.Debugf("Image uploaded") 273 } 274 return errors.Trace(err) 275 }