github.com/vmware/govmomi@v0.37.2/vmdk/import.go (about)

     1  /*
     2  Copyright (c) 2017 VMware, Inc. All Rights Reserved.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  nSee the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package vmdk
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/binary"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"os"
    27  	"path"
    28  	"path/filepath"
    29  	"strings"
    30  	"text/template"
    31  
    32  	"github.com/vmware/govmomi/object"
    33  	"github.com/vmware/govmomi/ovf"
    34  	"github.com/vmware/govmomi/vim25"
    35  	"github.com/vmware/govmomi/vim25/progress"
    36  	"github.com/vmware/govmomi/vim25/soap"
    37  	"github.com/vmware/govmomi/vim25/types"
    38  )
    39  
    40  var (
    41  	ErrInvalidFormat = errors.New("vmdk: invalid format (must be streamOptimized)")
    42  )
    43  
    44  // info is used to inspect a vmdk and generate an ovf template
    45  type info struct {
    46  	Header struct {
    47  		MagicNumber uint32
    48  		Version     uint32
    49  		Flags       uint32
    50  		Capacity    uint64
    51  	}
    52  
    53  	Capacity   uint64
    54  	Size       int64
    55  	Name       string
    56  	ImportName string
    57  }
    58  
    59  // stat looks at the vmdk header to make sure the format is streamOptimized and
    60  // extracts the disk capacity required to properly generate the ovf descriptor.
    61  func stat(name string) (*info, error) {
    62  	f, err := os.Open(filepath.Clean(name))
    63  	if err != nil {
    64  		return nil, err
    65  	}
    66  
    67  	var di info
    68  
    69  	var buf bytes.Buffer
    70  
    71  	_, err = io.CopyN(&buf, f, int64(binary.Size(di.Header)))
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  
    76  	fi, err := f.Stat()
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  
    81  	err = f.Close()
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  
    86  	err = binary.Read(&buf, binary.LittleEndian, &di.Header)
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  
    91  	if di.Header.MagicNumber != 0x564d444b { // SPARSE_MAGICNUMBER
    92  		return nil, ErrInvalidFormat
    93  	}
    94  
    95  	if di.Header.Flags&(1<<16) == 0 { // SPARSEFLAG_COMPRESSED
    96  		// Needs to be converted, for example:
    97  		//   vmware-vdiskmanager -r src.vmdk -t 5 dst.vmdk
    98  		//   qemu-img convert -O vmdk -o subformat=streamOptimized src.vmdk dst.vmdk
    99  		return nil, ErrInvalidFormat
   100  	}
   101  
   102  	di.Capacity = di.Header.Capacity * 512 // VMDK_SECTOR_SIZE
   103  	di.Size = fi.Size()
   104  	di.Name = filepath.Base(name)
   105  	di.ImportName = strings.TrimSuffix(di.Name, ".vmdk")
   106  
   107  	return &di, nil
   108  }
   109  
   110  // ovfenv is the minimal descriptor template required to import a vmdk
   111  var ovfenv = `<?xml version="1.0" encoding="UTF-8"?>
   112  <Envelope xmlns="http://schemas.dmtf.org/ovf/envelope/1"
   113            xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1"
   114            xmlns:cim="http://schemas.dmtf.org/wbem/wscim/1/common"
   115            xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"
   116            xmlns:vmw="http://www.vmware.com/schema/ovf"
   117            xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"
   118            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   119    <References>
   120      <File ovf:href="{{ .Name }}" ovf:id="file1" ovf:size="{{ .Size }}"/>
   121    </References>
   122    <DiskSection>
   123      <Info>Virtual disk information</Info>
   124      <Disk ovf:capacity="{{ .Capacity }}" ovf:capacityAllocationUnits="byte" ovf:diskId="vmdisk1" ovf:fileRef="file1" ovf:format="http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized" ovf:populatedSize="0"/>
   125    </DiskSection>
   126    <VirtualSystem ovf:id="{{ .ImportName }}">
   127      <Info>A virtual machine</Info>
   128      <Name>{{ .ImportName }}</Name>
   129      <OperatingSystemSection ovf:id="100" vmw:osType="other26xLinux64Guest">
   130        <Info>The kind of installed guest operating system</Info>
   131      </OperatingSystemSection>
   132      <VirtualHardwareSection>
   133        <Info>Virtual hardware requirements</Info>
   134        <System>
   135          <vssd:ElementName>Virtual Hardware Family</vssd:ElementName>
   136          <vssd:InstanceID>0</vssd:InstanceID>
   137          <vssd:VirtualSystemIdentifier>{{ .ImportName }}</vssd:VirtualSystemIdentifier>
   138          <vssd:VirtualSystemType>vmx-07</vssd:VirtualSystemType>
   139        </System>
   140        <Item>
   141          <rasd:AllocationUnits>hertz * 10^6</rasd:AllocationUnits>
   142          <rasd:Description>Number of Virtual CPUs</rasd:Description>
   143          <rasd:ElementName>1 virtual CPU(s)</rasd:ElementName>
   144          <rasd:InstanceID>1</rasd:InstanceID>
   145          <rasd:ResourceType>3</rasd:ResourceType>
   146          <rasd:VirtualQuantity>1</rasd:VirtualQuantity>
   147        </Item>
   148        <Item>
   149          <rasd:AllocationUnits>byte * 2^20</rasd:AllocationUnits>
   150          <rasd:Description>Memory Size</rasd:Description>
   151          <rasd:ElementName>1024MB of memory</rasd:ElementName>
   152          <rasd:InstanceID>2</rasd:InstanceID>
   153          <rasd:ResourceType>4</rasd:ResourceType>
   154          <rasd:VirtualQuantity>1024</rasd:VirtualQuantity>
   155        </Item>
   156        <Item>
   157          <rasd:Address>0</rasd:Address>
   158          <rasd:Description>SCSI Controller</rasd:Description>
   159          <rasd:ElementName>SCSI Controller 0</rasd:ElementName>
   160          <rasd:InstanceID>3</rasd:InstanceID>
   161          <rasd:ResourceSubType>VirtualSCSI</rasd:ResourceSubType>
   162          <rasd:ResourceType>6</rasd:ResourceType>
   163        </Item>
   164        <Item>
   165          <rasd:AddressOnParent>0</rasd:AddressOnParent>
   166          <rasd:ElementName>Hard Disk 1</rasd:ElementName>
   167          <rasd:HostResource>ovf:/disk/vmdisk1</rasd:HostResource>
   168          <rasd:InstanceID>9</rasd:InstanceID>
   169          <rasd:Parent>3</rasd:Parent>
   170          <rasd:ResourceType>17</rasd:ResourceType>
   171          <vmw:Config ovf:required="false" vmw:key="backing.writeThrough" vmw:value="false"/>
   172        </Item>
   173      </VirtualHardwareSection>
   174    </VirtualSystem>
   175  </Envelope>`
   176  
   177  // ovf returns an expanded descriptor template
   178  func (di *info) ovf() (string, error) {
   179  	var buf bytes.Buffer
   180  
   181  	tmpl, err := template.New("ovf").Parse(ovfenv)
   182  	if err != nil {
   183  		return "", err
   184  	}
   185  
   186  	err = tmpl.Execute(&buf, di)
   187  	if err != nil {
   188  		return "", err
   189  	}
   190  
   191  	return buf.String(), nil
   192  }
   193  
   194  // ImportParams contains the set of optional params to the Import function.
   195  // Note that "optional" may depend on environment, such as ESX or vCenter.
   196  type ImportParams struct {
   197  	Path       string
   198  	Logger     progress.Sinker
   199  	Type       types.VirtualDiskType
   200  	Force      bool
   201  	Datacenter *object.Datacenter
   202  	Pool       *object.ResourcePool
   203  	Folder     *object.Folder
   204  	Host       *object.HostSystem
   205  }
   206  
   207  // Import uploads a local vmdk file specified by name to the given datastore.
   208  func Import(ctx context.Context, c *vim25.Client, name string, datastore *object.Datastore, p ImportParams) error {
   209  	m := ovf.NewManager(c)
   210  	fm := datastore.NewFileManager(p.Datacenter, p.Force)
   211  
   212  	disk, err := stat(name)
   213  	if err != nil {
   214  		return err
   215  	}
   216  
   217  	var rename string
   218  
   219  	p.Path = strings.TrimSuffix(p.Path, "/")
   220  	if p.Path != "" {
   221  		disk.ImportName = p.Path
   222  		rename = path.Join(disk.ImportName, disk.Name)
   223  	}
   224  
   225  	// "target" is the path that will be created by ImportVApp()
   226  	// ImportVApp uses the same name for the VM and the disk.
   227  	target := fmt.Sprintf("%s/%s.vmdk", disk.ImportName, disk.ImportName)
   228  
   229  	if _, err = datastore.Stat(ctx, target); err == nil {
   230  		if p.Force {
   231  			// If we don't delete, the nfc upload adds a file name suffix
   232  			if err = fm.Delete(ctx, target); err != nil {
   233  				return err
   234  			}
   235  		} else {
   236  			return fmt.Errorf("%s: %s", os.ErrExist, datastore.Path(target))
   237  		}
   238  	}
   239  
   240  	// If we need to rename at the end, check if the file exists early unless Force.
   241  	if !p.Force && rename != "" {
   242  		if _, err = datastore.Stat(ctx, rename); err == nil {
   243  			return fmt.Errorf("%s: %s", os.ErrExist, datastore.Path(rename))
   244  		}
   245  	}
   246  
   247  	// Expand the ovf template
   248  	descriptor, err := disk.ovf()
   249  	if err != nil {
   250  		return err
   251  	}
   252  
   253  	pool := p.Pool     // TODO: use datastore to derive a default
   254  	folder := p.Folder // TODO: use datacenter to derive a default
   255  
   256  	kind := p.Type
   257  	if kind == "" {
   258  		kind = types.VirtualDiskTypeThin
   259  	}
   260  
   261  	params := types.OvfCreateImportSpecParams{
   262  		DiskProvisioning: string(kind),
   263  		EntityName:       disk.ImportName,
   264  	}
   265  
   266  	spec, err := m.CreateImportSpec(ctx, descriptor, pool, datastore, params)
   267  	if err != nil {
   268  		return err
   269  	}
   270  	if spec.Error != nil {
   271  		return errors.New(spec.Error[0].LocalizedMessage)
   272  	}
   273  
   274  	lease, err := pool.ImportVApp(ctx, spec.ImportSpec, folder, p.Host)
   275  	if err != nil {
   276  		return err
   277  	}
   278  
   279  	info, err := lease.Wait(ctx, spec.FileItem)
   280  	if err != nil {
   281  		return err
   282  	}
   283  
   284  	f, err := os.Open(filepath.Clean(name))
   285  	if err != nil {
   286  		return err
   287  	}
   288  
   289  	opts := soap.Upload{
   290  		ContentLength: disk.Size,
   291  		Progress:      p.Logger,
   292  	}
   293  
   294  	u := lease.StartUpdater(ctx, info)
   295  	defer u.Done()
   296  
   297  	item := info.Items[0] // we only have 1 disk to upload
   298  
   299  	err = lease.Upload(ctx, item, f, opts)
   300  	if err != nil {
   301  		return err
   302  	}
   303  
   304  	err = f.Close()
   305  	if err != nil {
   306  		return err
   307  	}
   308  
   309  	if err = lease.Complete(ctx); err != nil {
   310  		return err
   311  	}
   312  
   313  	// ImportVApp created a VM, here we detach the vmdk, then delete the VM.
   314  	vm := object.NewVirtualMachine(c, info.Entity)
   315  
   316  	device, err := vm.Device(ctx)
   317  	if err != nil {
   318  		return err
   319  	}
   320  
   321  	device = device.SelectByType((*types.VirtualDisk)(nil))
   322  
   323  	err = vm.RemoveDevice(ctx, true, device...)
   324  	if err != nil {
   325  		return err
   326  	}
   327  
   328  	task, err := vm.Destroy(ctx)
   329  	if err != nil {
   330  		return err
   331  	}
   332  
   333  	if err = task.Wait(ctx); err != nil {
   334  		return err
   335  	}
   336  
   337  	if rename == "" {
   338  		return nil
   339  	}
   340  
   341  	return fm.Move(ctx, target, rename)
   342  }