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  }