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  }