github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/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  	"encoding/base64"
    11  	"io"
    12  	"io/ioutil"
    13  	"net/http"
    14  	"net/url"
    15  	"os"
    16  	"path/filepath"
    17  
    18  	"github.com/juju/errors"
    19  	"github.com/juju/govmomi/object"
    20  	"github.com/juju/govmomi/vim25/progress"
    21  	"github.com/juju/govmomi/vim25/soap"
    22  	"github.com/juju/govmomi/vim25/types"
    23  	"github.com/juju/juju/juju/osenv"
    24  	"golang.org/x/net/context"
    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: base64.StdEncoding.EncodeToString(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  }