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  }