github.com/vmware/govmomi@v0.37.1/govc/importx/ovf.go (about)

     1  /*
     2  Copyright (c) 2014-2023 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 importx
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"errors"
    23  	"flag"
    24  	"fmt"
    25  	"path"
    26  	"strings"
    27  
    28  	"github.com/vmware/govmomi/find"
    29  	"github.com/vmware/govmomi/govc/cli"
    30  	"github.com/vmware/govmomi/govc/flags"
    31  	"github.com/vmware/govmomi/nfc"
    32  	"github.com/vmware/govmomi/object"
    33  	"github.com/vmware/govmomi/ovf"
    34  	"github.com/vmware/govmomi/vim25"
    35  	"github.com/vmware/govmomi/vim25/soap"
    36  	"github.com/vmware/govmomi/vim25/types"
    37  )
    38  
    39  type ovfx struct {
    40  	*flags.DatastoreFlag
    41  	*flags.HostSystemFlag
    42  	*flags.OutputFlag
    43  	*flags.ResourcePoolFlag
    44  	*flags.FolderFlag
    45  
    46  	*ArchiveFlag
    47  	*OptionsFlag
    48  
    49  	Name           string
    50  	VerifyManifest bool
    51  	Hidden         bool
    52  
    53  	Client       *vim25.Client
    54  	Datacenter   *object.Datacenter
    55  	Datastore    *object.Datastore
    56  	ResourcePool *object.ResourcePool
    57  }
    58  
    59  func init() {
    60  	cli.Register("import.ovf", &ovfx{})
    61  }
    62  
    63  func (cmd *ovfx) Register(ctx context.Context, f *flag.FlagSet) {
    64  	cmd.DatastoreFlag, ctx = flags.NewDatastoreFlag(ctx)
    65  	cmd.DatastoreFlag.Register(ctx, f)
    66  	cmd.HostSystemFlag, ctx = flags.NewHostSystemFlag(ctx)
    67  	cmd.HostSystemFlag.Register(ctx, f)
    68  	cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx)
    69  	cmd.OutputFlag.Register(ctx, f)
    70  	cmd.ResourcePoolFlag, ctx = flags.NewResourcePoolFlag(ctx)
    71  	cmd.ResourcePoolFlag.Register(ctx, f)
    72  	cmd.FolderFlag, ctx = flags.NewFolderFlag(ctx)
    73  	cmd.FolderFlag.Register(ctx, f)
    74  
    75  	cmd.ArchiveFlag, ctx = newArchiveFlag(ctx)
    76  	cmd.ArchiveFlag.Register(ctx, f)
    77  	cmd.OptionsFlag, ctx = newOptionsFlag(ctx)
    78  	cmd.OptionsFlag.Register(ctx, f)
    79  
    80  	f.StringVar(&cmd.Name, "name", "", "Name to use for new entity")
    81  	f.BoolVar(&cmd.VerifyManifest, "m", false, "Verify checksum of uploaded files against manifest (.mf)")
    82  	f.BoolVar(&cmd.Hidden, "hidden", false, "Enable hidden properties")
    83  }
    84  
    85  func (cmd *ovfx) Process(ctx context.Context) error {
    86  	if err := cmd.DatastoreFlag.Process(ctx); err != nil {
    87  		return err
    88  	}
    89  	if err := cmd.HostSystemFlag.Process(ctx); err != nil {
    90  		return err
    91  	}
    92  	if err := cmd.OutputFlag.Process(ctx); err != nil {
    93  		return err
    94  	}
    95  	if err := cmd.ResourcePoolFlag.Process(ctx); err != nil {
    96  		return err
    97  	}
    98  	if err := cmd.ArchiveFlag.Process(ctx); err != nil {
    99  		return err
   100  	}
   101  	if err := cmd.OptionsFlag.Process(ctx); err != nil {
   102  		return err
   103  	}
   104  	if err := cmd.FolderFlag.Process(ctx); err != nil {
   105  		return err
   106  	}
   107  	return nil
   108  }
   109  
   110  func (cmd *ovfx) Usage() string {
   111  	return "PATH_TO_OVF"
   112  }
   113  
   114  func (cmd *ovfx) Run(ctx context.Context, f *flag.FlagSet) error {
   115  	fpath, err := cmd.Prepare(f)
   116  	if err != nil {
   117  		return err
   118  	}
   119  
   120  	archive := &FileArchive{Path: fpath}
   121  	archive.Client = cmd.Client
   122  
   123  	cmd.Archive = archive
   124  
   125  	moref, err := cmd.Import(fpath)
   126  	if err != nil {
   127  		return err
   128  	}
   129  
   130  	vm := object.NewVirtualMachine(cmd.Client, *moref)
   131  	return cmd.Deploy(vm, cmd.OutputFlag)
   132  }
   133  
   134  func (cmd *ovfx) Prepare(f *flag.FlagSet) (string, error) {
   135  	var err error
   136  
   137  	args := f.Args()
   138  	if len(args) != 1 {
   139  		return "", errors.New("no file specified")
   140  	}
   141  
   142  	cmd.Client, err = cmd.DatastoreFlag.Client()
   143  	if err != nil {
   144  		return "", err
   145  	}
   146  
   147  	cmd.Datacenter, err = cmd.DatastoreFlag.Datacenter()
   148  	if err != nil {
   149  		return "", err
   150  	}
   151  
   152  	cmd.Datastore, err = cmd.DatastoreFlag.Datastore()
   153  	if err != nil {
   154  		return "", err
   155  	}
   156  
   157  	cmd.ResourcePool, err = cmd.ResourcePoolFlag.ResourcePoolIfSpecified()
   158  	if err != nil {
   159  		return "", err
   160  	}
   161  
   162  	return f.Arg(0), nil
   163  }
   164  
   165  func (cmd *ovfx) Map(op []Property) (p []types.KeyValue) {
   166  	for _, v := range op {
   167  		p = append(p, types.KeyValue{
   168  			Key:   v.Key,
   169  			Value: v.Value,
   170  		})
   171  	}
   172  
   173  	return
   174  }
   175  
   176  func (cmd *ovfx) validateNetwork(e *ovf.Envelope, net Network) {
   177  	var names []string
   178  
   179  	if e.Network != nil {
   180  		for _, n := range e.Network.Networks {
   181  			if n.Name == net.Name {
   182  				return
   183  			}
   184  			names = append(names, n.Name)
   185  		}
   186  	}
   187  
   188  	_, _ = cmd.Log(fmt.Sprintf("Warning: invalid NetworkMapping.Name=%q, valid names=%s\n", net.Name, names))
   189  }
   190  
   191  func (cmd *ovfx) NetworkMap(e *ovf.Envelope) ([]types.OvfNetworkMapping, error) {
   192  	ctx := context.TODO()
   193  	finder, err := cmd.DatastoreFlag.Finder()
   194  	if err != nil {
   195  		return nil, err
   196  	}
   197  
   198  	var nmap []types.OvfNetworkMapping
   199  	for _, m := range cmd.Options.NetworkMapping {
   200  		if m.Network == "" {
   201  			continue // Not set, let vSphere choose the default network
   202  		}
   203  		cmd.validateNetwork(e, m)
   204  
   205  		var ref types.ManagedObjectReference
   206  
   207  		net, err := finder.Network(ctx, m.Network)
   208  		if err != nil {
   209  			switch err.(type) {
   210  			case *find.NotFoundError:
   211  				if !ref.FromString(m.Network) {
   212  					return nil, err
   213  				} // else this is a raw MO ref
   214  			default:
   215  				return nil, err
   216  			}
   217  		} else {
   218  			ref = net.Reference()
   219  		}
   220  
   221  		nmap = append(nmap, types.OvfNetworkMapping{
   222  			Name:    m.Name,
   223  			Network: ref,
   224  		})
   225  	}
   226  
   227  	return nmap, err
   228  }
   229  
   230  func (cmd *ovfx) Import(fpath string) (*types.ManagedObjectReference, error) {
   231  	ctx := context.TODO()
   232  
   233  	o, err := cmd.ReadOvf(fpath)
   234  	if err != nil {
   235  		return nil, err
   236  	}
   237  
   238  	e, err := cmd.ReadEnvelope(o)
   239  	if err != nil {
   240  		return nil, fmt.Errorf("failed to parse ovf: %s", err)
   241  	}
   242  
   243  	name := "Govc Virtual Appliance"
   244  	if e.VirtualSystem != nil {
   245  		name = e.VirtualSystem.ID
   246  		if e.VirtualSystem.Name != nil {
   247  			name = *e.VirtualSystem.Name
   248  		}
   249  
   250  		if cmd.Hidden {
   251  			// TODO: userConfigurable is optional and defaults to false, so we should *add* userConfigurable=true
   252  			// if not set for a Property. But, there'd be a bunch more work involved to preserve other data in doing
   253  			// a complete xml.Marshal of the .ovf
   254  			o = bytes.ReplaceAll(o, []byte(`userConfigurable="false"`), []byte(`userConfigurable="true"`))
   255  		}
   256  	}
   257  
   258  	// Override name from options if specified
   259  	if cmd.Options.Name != nil {
   260  		name = *cmd.Options.Name
   261  	}
   262  
   263  	// Override name from arguments if specified
   264  	if cmd.Name != "" {
   265  		name = cmd.Name
   266  	}
   267  
   268  	nmap, err := cmd.NetworkMap(e)
   269  	if err != nil {
   270  		return nil, err
   271  	}
   272  
   273  	cisp := types.OvfCreateImportSpecParams{
   274  		DiskProvisioning:   cmd.Options.DiskProvisioning,
   275  		EntityName:         name,
   276  		IpAllocationPolicy: cmd.Options.IPAllocationPolicy,
   277  		IpProtocol:         cmd.Options.IPProtocol,
   278  		OvfManagerCommonParams: types.OvfManagerCommonParams{
   279  			DeploymentOption: cmd.Options.Deployment,
   280  			Locale:           "US"},
   281  		PropertyMapping: cmd.Map(cmd.Options.PropertyMapping),
   282  		NetworkMapping:  nmap,
   283  	}
   284  
   285  	host, err := cmd.HostSystemIfSpecified()
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  
   290  	if cmd.ResourcePool == nil {
   291  		if host == nil {
   292  			cmd.ResourcePool, err = cmd.ResourcePoolFlag.ResourcePool()
   293  		} else {
   294  			cmd.ResourcePool, err = host.ResourcePool(ctx)
   295  		}
   296  		if err != nil {
   297  			return nil, err
   298  		}
   299  	}
   300  
   301  	m := ovf.NewManager(cmd.Client)
   302  	spec, err := m.CreateImportSpec(ctx, string(o), cmd.ResourcePool, cmd.Datastore, cisp)
   303  	if err != nil {
   304  		return nil, err
   305  	}
   306  	if spec.Error != nil {
   307  		return nil, errors.New(spec.Error[0].LocalizedMessage)
   308  	}
   309  	if spec.Warning != nil {
   310  		for _, w := range spec.Warning {
   311  			_, _ = cmd.Log(fmt.Sprintf("Warning: %s\n", w.LocalizedMessage))
   312  		}
   313  	}
   314  
   315  	if cmd.Options.Annotation != "" {
   316  		switch s := spec.ImportSpec.(type) {
   317  		case *types.VirtualMachineImportSpec:
   318  			s.ConfigSpec.Annotation = cmd.Options.Annotation
   319  		case *types.VirtualAppImportSpec:
   320  			s.VAppConfigSpec.Annotation = cmd.Options.Annotation
   321  		}
   322  	}
   323  
   324  	var folder *object.Folder
   325  	// The folder argument must not be set on a VM in a vApp, otherwise causes
   326  	// InvalidArgument fault: A specified parameter was not correct: pool
   327  	if cmd.ResourcePool.Reference().Type != "VirtualApp" {
   328  		folder, err = cmd.FolderOrDefault("vm")
   329  		if err != nil {
   330  			return nil, err
   331  		}
   332  	}
   333  
   334  	if cmd.VerifyManifest {
   335  		err = cmd.readManifest(fpath)
   336  		if err != nil {
   337  			return nil, err
   338  		}
   339  	}
   340  
   341  	lease, err := cmd.ResourcePool.ImportVApp(ctx, spec.ImportSpec, folder, host)
   342  	if err != nil {
   343  		return nil, err
   344  	}
   345  
   346  	info, err := lease.Wait(ctx, spec.FileItem)
   347  	if err != nil {
   348  		return nil, err
   349  	}
   350  
   351  	u := lease.StartUpdater(ctx, info)
   352  	defer u.Done()
   353  
   354  	for _, i := range info.Items {
   355  		err = cmd.Upload(ctx, lease, i)
   356  		if err != nil {
   357  			return nil, err
   358  		}
   359  	}
   360  
   361  	return &info.Entity, lease.Complete(ctx)
   362  }
   363  
   364  func (cmd *ovfx) Upload(ctx context.Context, lease *nfc.Lease, item nfc.FileItem) error {
   365  	file := item.Path
   366  
   367  	f, size, err := cmd.Open(file)
   368  	if err != nil {
   369  		return err
   370  	}
   371  	defer f.Close()
   372  
   373  	logger := cmd.ProgressLogger(fmt.Sprintf("Uploading %s... ", path.Base(file)))
   374  	defer logger.Wait()
   375  
   376  	opts := soap.Upload{
   377  		ContentLength: size,
   378  		Progress:      logger,
   379  	}
   380  
   381  	err = lease.Upload(ctx, item, f, opts)
   382  	if err != nil {
   383  		return err
   384  	}
   385  
   386  	if cmd.VerifyManifest {
   387  		mapImportKeyToKey := func(urls []types.HttpNfcLeaseDeviceUrl, importKey string) string {
   388  			for _, url := range urls {
   389  				if url.ImportKey == importKey {
   390  					return url.Key
   391  				}
   392  			}
   393  			return ""
   394  		}
   395  		leaseInfo, err := lease.Wait(ctx, nil)
   396  		if err != nil {
   397  			return err
   398  		}
   399  		return cmd.validateChecksum(ctx, lease, file, mapImportKeyToKey(leaseInfo.DeviceUrl, item.DeviceId))
   400  	}
   401  	return nil
   402  }
   403  
   404  func (cmd *ovfx) validateChecksum(ctx context.Context, lease *nfc.Lease, file string, key string) error {
   405  	sum, found := cmd.manifest[file]
   406  	if !found {
   407  		msg := fmt.Sprintf("missing checksum for %v in manifest file", file)
   408  		return errors.New(msg)
   409  	}
   410  	// Perform the checksum match eagerly, after each file upload, instead
   411  	// of after uploading all the files, to provide fail-fast behavior.
   412  	// (Trade-off here is multiple GetManifest() API calls to the server.)
   413  	manifests, err := lease.GetManifest(ctx)
   414  	if err != nil {
   415  		return err
   416  	}
   417  	for _, m := range manifests {
   418  		if m.Key == key {
   419  			// Compare server-side computed checksum of uploaded file
   420  			// against the client's manifest entry (assuming client's
   421  			// manifest has correct checksums - client doesn't compute
   422  			// checksum of the file before uploading).
   423  
   424  			// Try matching sha1 first (newer versions have moved to sha256).
   425  			if strings.ToUpper(sum.Algorithm) == "SHA1" {
   426  				if sum.Checksum != m.Sha1 {
   427  					msg := fmt.Sprintf("manifest checksum %v mismatch with uploaded checksum %v for file %v",
   428  						sum.Checksum, m.Sha1, file)
   429  					return errors.New(msg)
   430  				}
   431  				// Uploaded file checksum computed by server matches with local manifest entry.
   432  				return nil
   433  			}
   434  			// If not sha1, check for other types (in a separate field).
   435  			if !strings.EqualFold(sum.Algorithm, m.ChecksumType) {
   436  				msg := fmt.Sprintf("manifest checksum type %v mismatch with uploaded checksum type %v for file %v",
   437  					sum.Algorithm, m.ChecksumType, file)
   438  				return errors.New(msg)
   439  			}
   440  			if !strings.EqualFold(sum.Checksum, m.Checksum) {
   441  				msg := fmt.Sprintf("manifest checksum %v mismatch with uploaded checksum %v for file %v",
   442  					sum.Checksum, m.Checksum, file)
   443  				return errors.New(msg)
   444  			}
   445  			// Uploaded file checksum computed by server matches with local manifest entry.
   446  			return nil
   447  		}
   448  	}
   449  	msg := fmt.Sprintf("missing manifest entry on server for uploaded file %v (key %v), manifests=%#v", file, key, manifests)
   450  	return errors.New(msg)
   451  }