github.com/vmware/govmomi@v0.51.0/ovf/importer/importer.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 importer 6 7 import ( 8 "bytes" 9 "context" 10 "errors" 11 "fmt" 12 "path" 13 "path/filepath" 14 "strings" 15 16 "github.com/vmware/govmomi/find" 17 "github.com/vmware/govmomi/nfc" 18 "github.com/vmware/govmomi/object" 19 "github.com/vmware/govmomi/ovf" 20 "github.com/vmware/govmomi/task" 21 "github.com/vmware/govmomi/vapi/library" 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 type Importer struct { 29 Log progress.LogFunc 30 31 Name string 32 VerifyManifest bool 33 Hidden bool 34 35 Client *vim25.Client 36 Finder *find.Finder 37 Sinker progress.Sinker 38 39 Datacenter *object.Datacenter 40 Datastore *object.Datastore 41 ResourcePool *object.ResourcePool 42 Host *object.HostSystem 43 Folder *object.Folder 44 45 Archive Archive 46 Manifest map[string]*library.Checksum 47 } 48 49 func (imp *Importer) manifestPath(fpath string) string { 50 base := filepath.Base(fpath) 51 ext := filepath.Ext(base) 52 return filepath.Join(filepath.Dir(fpath), strings.Replace(base, ext, ".mf", 1)) 53 } 54 55 func (imp *Importer) ReadManifest(fpath string) error { 56 mf, _, err := imp.Archive.Open(imp.manifestPath(fpath)) 57 if err != nil { 58 msg := fmt.Sprintf("failed to read manifest %q: %s", mf, err) 59 return errors.New(msg) 60 } 61 imp.Manifest, err = library.ReadManifest(mf) 62 _ = mf.Close() 63 return err 64 } 65 66 func (imp *Importer) ImportVApp(ctx context.Context, fpath string, opts Options) (*nfc.LeaseInfo, *nfc.Lease, error) { 67 o, err := ReadOvf(fpath, imp.Archive) 68 if err != nil { 69 return nil, nil, err 70 } 71 72 e, err := ReadEnvelope(o) 73 if err != nil { 74 return nil, nil, fmt.Errorf("failed to parse ovf: %s", err) 75 } 76 77 if e.VirtualSystem != nil { 78 if e.VirtualSystem != nil { 79 if opts.Name == nil { 80 opts.Name = &e.VirtualSystem.ID 81 if e.VirtualSystem.Name != nil { 82 opts.Name = e.VirtualSystem.Name 83 } 84 } 85 } 86 if imp.Hidden { 87 // TODO: userConfigurable is optional and defaults to false, so we should *add* userConfigurable=true 88 // if not set for a Property. But, there'd be a bunch more work involved to preserve other data in doing 89 // a complete xml.Marshal of the .ovf 90 o = bytes.ReplaceAll(o, []byte(`userConfigurable="false"`), []byte(`userConfigurable="true"`)) 91 } 92 } 93 94 name := "Govc Virtual Appliance" 95 if opts.Name != nil { 96 name = *opts.Name 97 } 98 99 nmap, err := imp.NetworkMap(ctx, e, opts.NetworkMapping) 100 if err != nil { 101 return nil, nil, err 102 } 103 104 cisp := types.OvfCreateImportSpecParams{ 105 DiskProvisioning: opts.DiskProvisioning, 106 EntityName: name, 107 IpAllocationPolicy: opts.IPAllocationPolicy, 108 IpProtocol: opts.IPProtocol, 109 OvfManagerCommonParams: types.OvfManagerCommonParams{ 110 DeploymentOption: opts.Deployment, 111 Locale: "US"}, 112 PropertyMapping: OVFMap(opts.PropertyMapping), 113 NetworkMapping: nmap, 114 } 115 116 m := ovf.NewManager(imp.Client) 117 spec, err := m.CreateImportSpec(ctx, string(o), imp.ResourcePool, imp.Datastore, &cisp) 118 if err != nil { 119 return nil, nil, err 120 } 121 if spec.Error != nil { 122 return nil, nil, &task.Error{LocalizedMethodFault: &spec.Error[0]} 123 } 124 if spec.Warning != nil { 125 for _, w := range spec.Warning { 126 _, _ = imp.Log(fmt.Sprintf("Warning: %s\n", w.LocalizedMessage)) 127 } 128 } 129 130 if opts.Annotation != "" { 131 switch s := spec.ImportSpec.(type) { 132 case *types.VirtualMachineImportSpec: 133 s.ConfigSpec.Annotation = opts.Annotation 134 case *types.VirtualAppImportSpec: 135 s.VAppConfigSpec.Annotation = opts.Annotation 136 } 137 } 138 139 if imp.VerifyManifest { 140 if err := imp.ReadManifest(fpath); err != nil { 141 return nil, nil, err 142 } 143 } 144 145 lease, err := imp.ResourcePool.ImportVApp(ctx, spec.ImportSpec, imp.Folder, imp.Host) 146 if err != nil { 147 return nil, nil, err 148 } 149 150 info, err := lease.Wait(ctx, spec.FileItem) 151 if err != nil { 152 _ = lease.Abort(ctx, nil) 153 return nil, nil, err 154 } 155 156 return info, lease, nil 157 } 158 159 func (imp *Importer) Import(ctx context.Context, fpath string, opts Options) (*types.ManagedObjectReference, error) { 160 info, lease, err := imp.ImportVApp(ctx, fpath, opts) 161 if err != nil { 162 return nil, err 163 } 164 165 u := lease.StartUpdater(ctx, info) 166 defer u.Done() 167 168 for _, i := range info.Items { 169 if err := imp.Upload(ctx, lease, i); err != nil { 170 _ = lease.Abort(ctx, &types.LocalizedMethodFault{ 171 Fault: &types.FileFault{ 172 File: i.Path, 173 }, 174 }) 175 return nil, err 176 } 177 } 178 179 return &info.Entity, lease.Complete(ctx) 180 } 181 182 func (imp *Importer) NetworkMap(ctx context.Context, e *ovf.Envelope, networks []Network) ([]types.OvfNetworkMapping, error) { 183 var nmap []types.OvfNetworkMapping 184 for _, m := range networks { 185 if m.Network == "" { 186 continue // Not set, let vSphere choose the default network 187 } 188 if err := ValidateNetwork(e, m); err != nil && imp.Log != nil { 189 _, _ = imp.Log(err.Error() + "\n") 190 } 191 192 var ref types.ManagedObjectReference 193 194 net, err := imp.Finder.Network(ctx, m.Network) 195 if err != nil { 196 switch err.(type) { 197 case *find.NotFoundError: 198 if !ref.FromString(m.Network) { 199 return nil, err 200 } // else this is a raw MO ref 201 default: 202 return nil, err 203 } 204 } else { 205 ref = net.Reference() 206 } 207 208 nmap = append(nmap, types.OvfNetworkMapping{ 209 Name: m.Name, 210 Network: ref, 211 }) 212 } 213 214 return nmap, nil 215 } 216 217 func OVFMap(op []Property) (p []types.KeyValue) { 218 for _, v := range op { 219 p = append(p, types.KeyValue{ 220 Key: v.Key, 221 Value: v.Value, 222 }) 223 } 224 225 return 226 } 227 228 func ValidateNetwork(e *ovf.Envelope, net Network) error { 229 var names []string 230 231 if e.Network != nil { 232 for _, n := range e.Network.Networks { 233 if n.Name == net.Name { 234 return nil 235 } 236 names = append(names, n.Name) 237 } 238 } 239 240 return fmt.Errorf("warning: invalid NetworkMapping.Name=%q, valid names=%s", net.Name, names) 241 } 242 243 func ValidateChecksum(ctx context.Context, lease *nfc.Lease, sum *library.Checksum, file string, key string) error { 244 // Perform the checksum match eagerly, after each file upload, instead 245 // of after uploading all the files, to provide fail-fast behavior. 246 // (Trade-off here is multiple GetManifest() API calls to the server.) 247 manifests, err := lease.GetManifest(ctx) 248 if err != nil { 249 return err 250 } 251 for _, m := range manifests { 252 if m.Key == key { 253 // Compare server-side computed checksum of uploaded file 254 // against the client's manifest entry (assuming client's 255 // manifest has correct checksums - client doesn't compute 256 // checksum of the file before uploading). 257 258 // Try matching sha1 first (newer versions have moved to sha256). 259 if strings.ToUpper(sum.Algorithm) == "SHA1" { 260 if sum.Checksum != m.Sha1 { 261 msg := fmt.Sprintf("manifest checksum %v mismatch with uploaded checksum %v for file %v", 262 sum.Checksum, m.Sha1, file) 263 return errors.New(msg) 264 } 265 // Uploaded file checksum computed by server matches with local manifest entry. 266 return nil 267 } 268 // If not sha1, check for other types (in a separate field). 269 if !strings.EqualFold(sum.Algorithm, m.ChecksumType) { 270 msg := fmt.Sprintf("manifest checksum type %v mismatch with uploaded checksum type %v for file %v", 271 sum.Algorithm, m.ChecksumType, file) 272 return errors.New(msg) 273 } 274 if !strings.EqualFold(sum.Checksum, m.Checksum) { 275 msg := fmt.Sprintf("manifest checksum %v mismatch with uploaded checksum %v for file %v", 276 sum.Checksum, m.Checksum, file) 277 return errors.New(msg) 278 } 279 // Uploaded file checksum computed by server matches with local manifest entry. 280 return nil 281 } 282 } 283 msg := fmt.Sprintf("missing manifest entry on server for uploaded file %v (key %v), manifests=%#v", file, key, manifests) 284 return errors.New(msg) 285 } 286 287 func (imp *Importer) Upload(ctx context.Context, lease *nfc.Lease, item nfc.FileItem) error { 288 file := item.Path 289 290 f, size, err := imp.Archive.Open(file) 291 if err != nil { 292 return err 293 } 294 defer f.Close() 295 296 logger := progress.NewProgressLogger(imp.Log, fmt.Sprintf("Uploading %s... ", path.Base(file))) 297 defer logger.Wait() 298 299 opts := soap.Upload{ 300 ContentLength: size, 301 Progress: logger, 302 } 303 304 err = lease.Upload(ctx, item, f, opts) 305 if err != nil { 306 return err 307 } 308 309 if imp.VerifyManifest { 310 mapImportKeyToKey := func(urls []types.HttpNfcLeaseDeviceUrl, importKey string) string { 311 for _, url := range urls { 312 if url.ImportKey == importKey { 313 return url.Key 314 } 315 } 316 return "" 317 } 318 leaseInfo, err := lease.Wait(ctx, nil) 319 if err != nil { 320 return err 321 } 322 sum, ok := imp.Manifest[file] 323 if !ok { 324 return fmt.Errorf("missing checksum for %v in manifest file", file) 325 } 326 return ValidateChecksum(ctx, lease, sum, file, mapImportKeyToKey(leaseInfo.DeviceUrl, item.DeviceId)) 327 } 328 return nil 329 }