github.com/vmware/govmomi@v0.37.2/vapi/vm/simulator/simulator.go (about)

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