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, ¶ms) 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 }