github.com/vmware/govmomi@v0.51.0/vapi/vm/simulator/simulator.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 simulator
     6  
     7  import (
     8  	"context"
     9  	"log"
    10  	"net/http"
    11  	"net/url"
    12  	"strings"
    13  
    14  	"github.com/google/uuid"
    15  
    16  	"github.com/vmware/govmomi/simulator"
    17  	"github.com/vmware/govmomi/vapi/rest"
    18  	vapi "github.com/vmware/govmomi/vapi/simulator"
    19  	"github.com/vmware/govmomi/vapi/vm/dataset"
    20  	"github.com/vmware/govmomi/vapi/vm/internal"
    21  	"github.com/vmware/govmomi/vim25/methods"
    22  	"github.com/vmware/govmomi/vim25/types"
    23  )
    24  
    25  const (
    26  	// Minimal VM hardware version which supports DataSets feature
    27  	minVmHardwareVersionDataSets = "vmx-20"
    28  
    29  	typeVM = "VirtualMachine"
    30  )
    31  
    32  func init() {
    33  	simulator.RegisterEndpoint(func(s *simulator.Service, r *simulator.Registry) {
    34  		New(s.Listen).Register(s, r)
    35  	})
    36  }
    37  
    38  type Handler struct {
    39  	u        *url.URL
    40  	registry *simulator.Registry
    41  }
    42  
    43  func New(u *url.URL) *Handler {
    44  	h := &Handler{
    45  		u: u,
    46  	}
    47  	return h
    48  }
    49  
    50  const (
    51  	restPathPrefix = rest.Path + internal.LegacyVCenterVMPath + "/"
    52  	apiPathPrefix  = internal.VCenterVMPath + "/"
    53  )
    54  
    55  func (h *Handler) Register(s *simulator.Service, r *simulator.Registry) {
    56  	if r.IsVPX() {
    57  		h.registry = r
    58  		s.HandleFunc(restPathPrefix, h.handle)
    59  		s.HandleFunc(apiPathPrefix, h.handle)
    60  	}
    61  }
    62  
    63  // path starts with "/api/vcenter/vm"
    64  func (h *Handler) handle(w http.ResponseWriter, r *http.Request) {
    65  	// The standard http.ServeMux does not support placeholders, so traverse the path segment by segment.
    66  	// 'tail' tracks the remaining path segments.
    67  	p := r.URL.Path
    68  	p = strings.TrimPrefix(p, restPathPrefix)
    69  	p = strings.TrimPrefix(p, apiPathPrefix)
    70  	tail := strings.Split(p, "/")
    71  	if len(tail) == 0 {
    72  		// "/api/vcenter/vm"
    73  		http.NotFound(w, r)
    74  	} else {
    75  		// "/api/vcenter/vm/..."
    76  		switch tail[0] {
    77  		case "":
    78  			http.NotFound(w, r)
    79  			return
    80  		default:
    81  			vmId := tail[0]
    82  			h.handleVm(w, r, tail[1:], vmId)
    83  		}
    84  	}
    85  }
    86  
    87  // path starts with "/api/vcenter/vm/{}"
    88  func (h *Handler) handleVm(w http.ResponseWriter, r *http.Request, tail []string, vmId string) {
    89  	vm := h.validateVmExists(w, r, vmId)
    90  	if vm == nil {
    91  		return
    92  	}
    93  	ctx := &simulator.Context{
    94  		Context: context.Background(),
    95  		Session: &simulator.Session{
    96  			UserSession: types.UserSession{
    97  				Key: uuid.New().String(),
    98  			},
    99  			Registry: h.registry,
   100  		},
   101  		Map: h.registry,
   102  	}
   103  	h.registry.WithLock(ctx, vm.Reference(), func() {
   104  		if len(tail) == 0 {
   105  			// "/api/vcenter/vm/{}"
   106  			switch r.Method {
   107  			case http.MethodDelete:
   108  				h.deleteVM(w, r, ctx, vm)
   109  			default:
   110  				http.NotFound(w, r)
   111  			}
   112  		} else {
   113  			// "/api/vcenter/vm/{}/..."
   114  			switch tail[0] {
   115  			case "data-sets":
   116  				h.handleVmDataSets(w, r, tail[1:], vm)
   117  			default:
   118  				http.NotFound(w, r)
   119  			}
   120  		}
   121  	})
   122  }
   123  
   124  // path starts with "/api/vcenter/vm/{}/data-sets"
   125  func (h *Handler) handleVmDataSets(w http.ResponseWriter, r *http.Request, tail []string, vm *simulator.VirtualMachine) {
   126  	if !h.validateVmHardwareVersionDataSets(w, r, vm) {
   127  		return
   128  	}
   129  	if len(tail) == 0 {
   130  		// "/api/vcenter/vm/{}/data-sets"
   131  		switch r.Method {
   132  		case http.MethodGet:
   133  			h.listDataSets(w, r, vm)
   134  		case http.MethodPost:
   135  			h.createDataSet(w, r, vm)
   136  		default:
   137  			http.NotFound(w, r)
   138  		}
   139  	} else {
   140  		// "/api/vcenter/vm/{}/data-sets/..."
   141  		switch tail[0] {
   142  		case "":
   143  			http.NotFound(w, r)
   144  			return
   145  		default:
   146  			dataSetId := tail[0]
   147  			h.handleVmDataSet(w, r, tail[1:], vm, dataSetId)
   148  		}
   149  	}
   150  }
   151  
   152  // path starts with "/api/vcenter/vm/{}/data-sets/{}"
   153  func (h *Handler) handleVmDataSet(w http.ResponseWriter, r *http.Request, tail []string, vm *simulator.VirtualMachine, dataSetId string) {
   154  	dataSet := h.validateDataSetExists(w, r, vm, dataSetId)
   155  	if dataSet == nil {
   156  		return
   157  	}
   158  	if len(tail) == 0 {
   159  		// "/api/vcenter/vm/{}/data-sets/{}"
   160  		switch r.Method {
   161  		case http.MethodGet:
   162  			vapi.StatusOK(w, dataSet.Info)
   163  		case http.MethodPatch:
   164  			h.updateDataSet(w, r, vm, dataSet)
   165  		case http.MethodDelete:
   166  			h.deleteDataSet(w, r, vm, dataSet)
   167  		default:
   168  			http.NotFound(w, r)
   169  		}
   170  	} else {
   171  		// "/api/vcenter/vm/{}/data-sets/{}/..."
   172  		switch tail[0] {
   173  		case "entries":
   174  			h.handleVmDataSetEntries(w, r, tail[1:], vm, dataSet)
   175  		default:
   176  			http.NotFound(w, r)
   177  		}
   178  
   179  	}
   180  }
   181  
   182  // path starts with "/api/vcenter/vm/{}/data-sets/{}/entries"
   183  func (h *Handler) handleVmDataSetEntries(w http.ResponseWriter, r *http.Request, tail []string, vm *simulator.VirtualMachine, dataSet *simulator.DataSet) {
   184  	if len(tail) == 0 {
   185  		// "/api/vcenter/vm/{}/data-sets/{}/entries"
   186  		switch r.Method {
   187  		case http.MethodGet:
   188  			h.listDataSetEntries(w, r, vm, dataSet)
   189  		default:
   190  			http.NotFound(w, r)
   191  		}
   192  	} else {
   193  		// "/api/vcenter/vm/{}/data-sets/{}/entries/..."
   194  		switch tail[0] {
   195  		case "":
   196  			http.NotFound(w, r)
   197  			return
   198  		default:
   199  			entryKey := tail[0]
   200  			h.handleVmDataSetEntry(w, r, tail[1:], vm, dataSet, entryKey)
   201  		}
   202  	}
   203  }
   204  
   205  // path starts with "/api/vcenter/vm/{}/data-sets/{}/entries/{}"
   206  func (h *Handler) handleVmDataSetEntry(w http.ResponseWriter, r *http.Request, tail []string, vm *simulator.VirtualMachine, dataSet *simulator.DataSet, entryKey string) {
   207  	if len(tail) > 0 {
   208  		http.NotFound(w, r)
   209  		return
   210  	}
   211  	switch r.Method {
   212  	case http.MethodGet:
   213  		h.getDataSetEntry(w, r, vm, dataSet, entryKey)
   214  	case http.MethodPut:
   215  		h.setDataSetEntry(w, r, vm, dataSet, entryKey)
   216  	case http.MethodDelete:
   217  		h.deleteDataSetEntry(w, r, vm, dataSet, entryKey)
   218  	default:
   219  		http.NotFound(w, r)
   220  	}
   221  }
   222  
   223  func (h *Handler) deleteVM(w http.ResponseWriter, r *http.Request, ctx *simulator.Context, vm *simulator.VirtualMachine) {
   224  	taskRef := vm.DestroyTask(ctx, &types.Destroy_Task{This: vm.Self}).(*methods.Destroy_TaskBody).Res.Returnval
   225  	task := ctx.Map.Get(taskRef).(*simulator.Task)
   226  	task.Wait()
   227  	if task.Info.Error != nil {
   228  		log.Printf("%s %s: %v", r.Method, r.RequestURI, task.Info.Error)
   229  		vapi.ApiErrorGeneral(w)
   230  		return
   231  	}
   232  	vapi.StatusOK(w)
   233  }
   234  
   235  func (h *Handler) createDataSet(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine) {
   236  	if !h.validateVmIsNotSuspended(w, r, vm) {
   237  		return
   238  	}
   239  	var createSpec dataset.CreateSpec
   240  	if !vapi.Decode(r, w, &createSpec) {
   241  		return
   242  	}
   243  	if createSpec.Name == "" {
   244  		vapi.ApiErrorInvalidArgument(w)
   245  		return
   246  	}
   247  	if createSpec.Host == "" {
   248  		vapi.ApiErrorInvalidArgument(w)
   249  		return
   250  	}
   251  	if createSpec.Guest == "" {
   252  		vapi.ApiErrorInvalidArgument(w)
   253  		return
   254  	}
   255  	dataSetId := createSpec.Name
   256  	_, ok := vm.DataSets[dataSetId]
   257  	if ok {
   258  		vapi.ApiErrorAlreadyExists(w)
   259  		return
   260  	}
   261  	dataSet := &simulator.DataSet{
   262  		Info: &dataset.Info{
   263  			Name:                     createSpec.Name,
   264  			Description:              createSpec.Description,
   265  			Host:                     createSpec.Host,
   266  			Guest:                    createSpec.Guest,
   267  			Used:                     0,
   268  			OmitFromSnapshotAndClone: getOrDefault(createSpec.OmitFromSnapshotAndClone, false),
   269  		},
   270  		ID:      dataSetId,
   271  		Entries: make(map[string]string),
   272  	}
   273  	vm.DataSets[dataSetId] = dataSet
   274  	vapi.StatusOK(w, dataSetId)
   275  }
   276  
   277  func (h *Handler) listDataSets(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine) {
   278  	var result []dataset.Summary = make([]dataset.Summary, 0, len(vm.DataSets))
   279  	for _, v := range vm.DataSets {
   280  		summary := dataset.Summary{
   281  			DataSet:     v.ID,
   282  			Name:        v.Name,
   283  			Description: v.Description,
   284  		}
   285  		result = append(result, summary)
   286  	}
   287  	vapi.StatusOK(w, result)
   288  }
   289  
   290  func (h *Handler) deleteDataSet(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSet *simulator.DataSet) {
   291  	if !h.validateVmIsNotSuspended(w, r, vm) {
   292  		return
   293  	}
   294  	force := strings.EqualFold(r.URL.Query().Get("force"), "true")
   295  	if len(dataSet.Entries) > 0 && !force {
   296  		// cannot delete non-empty data set without force
   297  		vapi.ApiErrorResourceInUse(w)
   298  		return
   299  	}
   300  	delete(vm.DataSets, dataSet.ID)
   301  	vapi.StatusOK(w)
   302  }
   303  
   304  func (h *Handler) updateDataSet(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSet *simulator.DataSet) {
   305  	if !h.validateVmIsNotSuspended(w, r, vm) {
   306  		return
   307  	}
   308  	var updateSpec dataset.UpdateSpec
   309  	if !vapi.Decode(r, w, &updateSpec) {
   310  		return
   311  	}
   312  	if updateSpec.Description != nil {
   313  		dataSet.Description = *updateSpec.Description
   314  	}
   315  	if updateSpec.Host != nil {
   316  		dataSet.Host = *updateSpec.Host
   317  	}
   318  	if updateSpec.Guest != nil {
   319  		dataSet.Guest = *updateSpec.Guest
   320  	}
   321  	if updateSpec.OmitFromSnapshotAndClone != nil {
   322  		dataSet.OmitFromSnapshotAndClone = *updateSpec.OmitFromSnapshotAndClone
   323  	}
   324  	vapi.StatusOK(w)
   325  }
   326  
   327  func (h *Handler) listDataSetEntries(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSet *simulator.DataSet) {
   328  	if dataSet.Host == dataset.AccessNone {
   329  		vapi.ApiErrorUnauthorized(w)
   330  		return
   331  	}
   332  	var result []string = make([]string, 0, len(dataSet.Entries))
   333  	for k := range dataSet.Entries {
   334  		result = append(result, k)
   335  	}
   336  	vapi.StatusOK(w, result)
   337  }
   338  
   339  func (h *Handler) getDataSetEntry(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSet *simulator.DataSet, entryKey string) {
   340  	if dataSet.Host == dataset.AccessNone {
   341  		vapi.ApiErrorUnauthorized(w)
   342  		return
   343  	}
   344  	val, ok := dataSet.Entries[entryKey]
   345  	if !ok {
   346  		vapi.ApiErrorNotFound(w)
   347  		return
   348  	}
   349  	vapi.StatusOK(w, val)
   350  }
   351  
   352  const (
   353  	// A key can be at most 4096 bytes.
   354  	entryKeyMaxLen = 4096
   355  	// A value can be at most 1MB.
   356  	entryValueMaxLen = 1024 * 1024
   357  )
   358  
   359  func (h *Handler) setDataSetEntry(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSet *simulator.DataSet, entryKey string) {
   360  	var val string
   361  	if !vapi.Decode(r, w, &val) {
   362  		return
   363  	}
   364  	if !h.validateVmIsNotSuspended(w, r, vm) {
   365  		return
   366  	}
   367  	if dataSet.Host != dataset.AccessReadWrite {
   368  		vapi.ApiErrorUnauthorized(w)
   369  		return
   370  	}
   371  	if len(entryKey) > entryKeyMaxLen {
   372  		vapi.ApiErrorInvalidArgument(w)
   373  		return
   374  	}
   375  	if len(val) > entryValueMaxLen {
   376  		vapi.ApiErrorInvalidArgument(w)
   377  		return
   378  	}
   379  	old, ok := dataSet.Entries[entryKey]
   380  	if ok {
   381  		dataSet.Used -= len(entryKey)
   382  		dataSet.Used -= len(old)
   383  	}
   384  	dataSet.Entries[entryKey] = val
   385  	dataSet.Used += len(entryKey)
   386  	dataSet.Used += len(val)
   387  	vapi.StatusOK(w)
   388  }
   389  
   390  func (h *Handler) deleteDataSetEntry(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSet *simulator.DataSet, entryKey string) {
   391  	if !h.validateVmIsNotSuspended(w, r, vm) {
   392  		return
   393  	}
   394  	if dataSet.Host != dataset.AccessReadWrite {
   395  		vapi.ApiErrorUnauthorized(w)
   396  		return
   397  	}
   398  	val, ok := dataSet.Entries[entryKey]
   399  	if !ok {
   400  		vapi.ApiErrorNotFound(w)
   401  		return
   402  	}
   403  	dataSet.Used -= len(entryKey)
   404  	dataSet.Used -= len(val)
   405  	delete(dataSet.Entries, entryKey)
   406  	vapi.StatusOK(w)
   407  }
   408  
   409  func getOrDefault(b *bool, defaultValue bool) bool {
   410  	if b == nil {
   411  		return defaultValue
   412  	}
   413  	return *b
   414  }
   415  
   416  func (h *Handler) validateVmExists(w http.ResponseWriter, r *http.Request, vmId string) *simulator.VirtualMachine {
   417  	vm, ok := h.registry.Get(types.ManagedObjectReference{Type: typeVM, Value: vmId}).(*simulator.VirtualMachine)
   418  	if !ok {
   419  		vapi.ApiErrorNotFound(w)
   420  		return nil
   421  	}
   422  	return vm
   423  }
   424  
   425  func (h *Handler) validateDataSetExists(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSetId string) *simulator.DataSet {
   426  	dataSet, ok := vm.DataSets[dataSetId]
   427  	if !ok {
   428  		vapi.ApiErrorNotFound(w)
   429  		return nil
   430  	}
   431  	return dataSet
   432  }
   433  
   434  func (h *Handler) validateVmHardwareVersionDataSets(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine) bool {
   435  	version := vm.Config.Version
   436  	if len(version) < len(minVmHardwareVersionDataSets) {
   437  		vapi.ApiErrorUnsupported(w)
   438  		return false
   439  	}
   440  	if len(version) == len(minVmHardwareVersionDataSets) && version < minVmHardwareVersionDataSets {
   441  		vapi.ApiErrorUnsupported(w)
   442  		return false
   443  	}
   444  	return true
   445  }
   446  
   447  func (h *Handler) validateVmIsNotSuspended(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine) bool {
   448  	if vm.Summary.Runtime.PowerState == types.VirtualMachinePowerStateSuspended {
   449  		vapi.ApiErrorNotAllowedInCurrentState(w)
   450  		return false
   451  	}
   452  	return true
   453  }