github.com/vmware/govmomi@v0.43.0/ovf/importer/importer.go (about)

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