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