github.com/vmware/govmomi@v0.37.1/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 }