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

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